Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WASM: add bindings for Note #5680

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ironfish-rust-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod assets;
pub mod errors;
pub mod keys;
pub mod merkle_note;
pub mod note;
pub mod primitives;
pub mod transaction;

Expand Down
98 changes: 96 additions & 2 deletions ironfish-rust-wasm/src/merkle_note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use crate::{errors::IronfishError, primitives::Scalar, wasm_bindgen_wrapper};
use crate::{
errors::IronfishError,
keys::{IncomingViewKey, OutgoingViewKey},
note::Note,
primitives::Scalar,
wasm_bindgen_wrapper,
};
use wasm_bindgen::prelude::*;

wasm_bindgen_wrapper! {
Expand Down Expand Up @@ -30,6 +36,50 @@ impl MerkleNote {
pub fn merkle_hash(&self) -> MerkleNoteHash {
self.0.merkle_hash().into()
}

#[wasm_bindgen(js_name = decryptNoteForOwner)]
pub fn decrypt_note_for_owner(
&self,
owner_view_key: &IncomingViewKey,
) -> Result<Note, IronfishError> {
self.0
.decrypt_note_for_owner(owner_view_key.as_ref())
.map(|n| n.into())
.map_err(|e| e.into())
}

#[wasm_bindgen(js_name = decryptNoteForOwners)]
pub fn decrypt_note_for_owners(&self, owner_view_keys: Vec<IncomingViewKey>) -> Vec<Note> {
// The original `decrypt_note_for_owners` returns a `Vec<Result<Note, E>>`. Here instead we
// are filtering out all errors. This likely makes this method hard to use in practice,
// because the information for mapping between the original owner and the resulting note is
// lost. However, returing a `Vec<Result>` or a `Vec<Option>` is currently unsupported in
// wasm-bindgen, so offering equivalent functionality requires a new custom type, which can
// be implemented at a later date.
self.0
.decrypt_note_for_owners(
owner_view_keys
.into_iter()
.map(|k| k.into())
.collect::<Vec<_>>()
.as_slice(),
)
.into_iter()
.filter_map(Result::ok)
.map(|n| n.into())
.collect()
}

#[wasm_bindgen(js_name = decryptNoteForSpender)]
pub fn decrypt_note_for_spender(
&self,
spender_key: &OutgoingViewKey,
) -> Result<Note, IronfishError> {
self.0
.decrypt_note_for_spender(spender_key.as_ref())
.map(|n| n.into())
.map_err(|e| e.into())
}
}

wasm_bindgen_wrapper! {
Expand Down Expand Up @@ -72,10 +122,29 @@ impl MerkleNoteHash {

#[cfg(test)]
mod tests {
use crate::merkle_note::MerkleNoteHash;
use crate::{
keys::SaplingKey,
merkle_note::{MerkleNote, MerkleNoteHash},
};
use hex_literal::hex;
use wasm_bindgen_test::wasm_bindgen_test;

// Merkle note copied from one of the fixtures in the `ironfish` NodeJS package
const TEST_MERKLE_NOTE_BYTES: [u8; 329] = hex!(
"d76e3e7f7f85065f696d6e3587450d8386ae9a62b61aa664eec642afdea715a4b1aaff663825233b351fb068cf\
7a9fcba6d17be2f2e56c9848ea1ce44ce0955e0d32fcadc2c462235e7b626bf3b0d6b6dcc261816efebb42b5040\
947546e95d898ab93198324e66d612e6975e26ea1f4356b294b6994a18fd76a0b829f8ab94576026110768de2cc\
ff9aea405331b128edd905049d286283cc0a0db6302801f8be21b3767ed2ff36b2c7a712f46f89d2e87647e55b4\
97225daf24719a713bce9a8522c62f1fb04e6ce67a966da2641d98e9e03a0da925f720040b48acfc30b64e91ca9\
f119dcef85ea8bfbc906f203fc1351550995988a3549726f6e2046697368206e6f746520656e6372797074696f6\
e206d696e6572206b65793030303030303030303030303030303030303030303030303030303030303030303030\
3030303030303030303000"
);

// Key that owns the above note
const TEST_SAPLING_KEY_BYTES: [u8; 32] =
hex!("2301a28b5c47a79d328e11485647b3da876678028d8312dc40e726e8e118fe1a");

#[test]
#[wasm_bindgen_test]
fn combine_hash() {
Expand All @@ -93,4 +162,29 @@ mod tests {
hex!("65fa868a24f39bead19143c23b7c37c6966bec5cf5e60269cb7964d407fe3d47")
);
}

#[test]
#[wasm_bindgen_test]
fn decrypt_note_for_owner() {
let key = SaplingKey::deserialize(TEST_SAPLING_KEY_BYTES.as_slice()).unwrap();
let merkle_note = MerkleNote::deserialize(TEST_MERKLE_NOTE_BYTES.as_slice())
.expect("deserialization failed");
let note = merkle_note
.decrypt_note_for_owner(&key.incoming_view_key())
.expect("decryption failed");

assert_eq!(note.value(), 2_000_000_000);
}

#[test]
#[wasm_bindgen_test]
fn decrypt_note_for_owners() {
let key = SaplingKey::deserialize(TEST_SAPLING_KEY_BYTES.as_slice()).unwrap();
let merkle_note = MerkleNote::deserialize(TEST_MERKLE_NOTE_BYTES.as_slice())
.expect("deserialization failed");
let notes = merkle_note.decrypt_note_for_owners(vec![key.incoming_view_key()]);

assert_eq!(notes.len(), 1);
assert_eq!(notes[0].value(), 2_000_000_000);
}
}
223 changes: 223 additions & 0 deletions ironfish-rust-wasm/src/note.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use crate::{
assets::AssetIdentifier,
errors::IronfishError,
keys::{IncomingViewKey, PublicAddress, ViewKey},
primitives::{ExtendedPoint, Nullifier},
wasm_bindgen_wrapper,
};
use ironfish::errors::IronfishErrorKind;
use wasm_bindgen::prelude::*;

wasm_bindgen_wrapper! {
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Note(ironfish::Note);
}

#[wasm_bindgen]
impl Note {
#[wasm_bindgen(constructor)]
pub fn deserialize(bytes: &[u8]) -> Result<Self, IronfishError> {
Ok(Self(ironfish::Note::read(bytes)?))
}

#[wasm_bindgen]
pub fn serialize(&self) -> Vec<u8> {
let mut buf = Vec::new();
self.0.write(&mut buf).expect("failed to serialize note");
buf
}

#[wasm_bindgen(js_name = fromParts)]
pub fn from_parts(
owner: PublicAddress,
value: u64,
memo: &str,
asset_id: AssetIdentifier,
sender: PublicAddress,
) -> Self {
Self(ironfish::Note::new(
owner.into(),
value,
memo,
asset_id.into(),
sender.into(),
))
}

#[wasm_bindgen(getter)]
pub fn value(&self) -> u64 {
self.0.value()
}

#[wasm_bindgen(getter)]
pub fn memo(&self) -> Vec<u8> {
self.0.memo().0.to_vec()
}

#[wasm_bindgen(getter)]
pub fn owner(&self) -> PublicAddress {
self.0.owner().into()
}

#[wasm_bindgen(getter)]
pub fn asset_generator(&self) -> ExtendedPoint {
self.0.asset_generator().into()
}

#[wasm_bindgen(getter)]
pub fn asset_id(&self) -> AssetIdentifier {
self.0.asset_id().to_owned().into()
}

#[wasm_bindgen(getter)]
pub fn sender(&self) -> PublicAddress {
self.0.sender().into()
}

#[wasm_bindgen(getter)]
pub fn commitment(&self) -> Vec<u8> {
self.0.commitment().to_vec()
}

#[wasm_bindgen]
pub fn encrypt(&self, shared_secret: &[u8]) -> Result<Vec<u8>, IronfishError> {
let shared_secret: &[u8; 32] = shared_secret
.try_into()
.map_err(|_| IronfishErrorKind::InvalidData)?;
Ok(self.0.encrypt(shared_secret).to_vec())
}

#[wasm_bindgen(js_name = fromOwnerEncrypted)]
pub fn from_owner_encrypted(
owner_view_key: &IncomingViewKey,
shared_secret: &[u8],
encrypted_bytes: &[u8],
) -> Result<Self, IronfishError> {
let shared_secret: &[u8; 32] = shared_secret
.try_into()
.map_err(|_| IronfishErrorKind::InvalidData)?;
let encrypted_bytes: &[u8; 152] = encrypted_bytes
.try_into()
.map_err(|_| IronfishErrorKind::InvalidData)?;
Ok(Self(ironfish::Note::from_owner_encrypted(
owner_view_key.as_ref(),
shared_secret,
encrypted_bytes,
)?))
}

#[wasm_bindgen]
pub fn nullifier(&self, view_key: &ViewKey, position: u64) -> Nullifier {
self.0.nullifier(view_key.as_ref(), position).into()
}
}

#[cfg(test)]
mod tests {
use crate::{
assets::AssetIdentifier,
keys::{PublicAddress, SaplingKey},
note::Note,
};
use hex_literal::hex;
use rand::{thread_rng, Rng};
use wasm_bindgen_test::wasm_bindgen_test;

const TEST_NOTE_BYTES: [u8; 168] = hex!(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0cccccccccccccccccccccccccc\
ccccccccccccccccccccccccccccccccccccc07b0000000000000000e2fb75515b55ed7f84be996ef80dae38b3d\
2076d1ffffd0970b641cde4060e736f6d65206d656d6fe29c8e0000000000000000000000000000000000000000\
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf"
);

#[test]
#[wasm_bindgen_test]
fn deserialize() {
let note = Note::deserialize(TEST_NOTE_BYTES.as_slice())
.expect("reading note should have succeeded");

assert_eq!(
note.owner().serialize(),
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0")
);
assert_eq!(
note.sender().serialize(),
hex!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf")
);
assert_eq!(note.value(), 123);
assert_eq!(
note.memo(),
b"some memo\xe2\x9c\x8e\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
);
assert_eq!(
note.asset_id().serialize(),
hex!("ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc0")
);
assert_eq!(
note.commitment(),
hex!("d044ae177718d5282807186168253e33a080e45a19be4cc27dc47b0a7146450d")
);
assert_eq!(note.serialize(), TEST_NOTE_BYTES);
}

#[test]
#[wasm_bindgen_test]
fn from_parts() {
let owner = PublicAddress::deserialize(
hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0").as_slice(),
)
.unwrap();
let sender = PublicAddress::deserialize(
hex!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbf").as_slice(),
)
.unwrap();
let asset_id = AssetIdentifier::deserialize(
hex!("ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc0").as_slice(),
)
.unwrap();

let note = Note::from_parts(
owner.clone(),
123,
"some memo✎",
asset_id.clone(),
sender.clone(),
);

assert_eq!(note.owner(), owner);
assert_eq!(note.value(), 123);
assert_eq!(
note.memo(),
b"some memo\xe2\x9c\x8e\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
);
assert_eq!(note.asset_id(), asset_id);
assert_eq!(note.sender(), sender);
}

#[test]
#[wasm_bindgen_test]
fn encrypt_decrypt_roundtrip() {
let owner_key = SaplingKey::random();
let sender_key = SaplingKey::random();
let note = Note::from_parts(
owner_key.public_address(),
123_456_789,
"memo",
AssetIdentifier::native(),
sender_key.public_address(),
);

let shared_secret: [u8; 32] = thread_rng().gen();
let encrypted = note.encrypt(&shared_secret).expect("encryption failed");

let decrypted =
Note::from_owner_encrypted(&owner_key.incoming_view_key(), &shared_secret, &encrypted)
.expect("decryption failed");

assert_eq!(decrypted, note);
}
}
2 changes: 1 addition & 1 deletion ironfish-rust/src/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl fmt::Display for Memo {
///
/// When receiving funds, a new note needs to be created for the new owner
/// to hold those funds.
#[derive(Debug, Clone)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Note {
/// Asset identifier the note is associated with
pub(crate) asset_id: AssetIdentifier,
Expand Down