Skip to content

Commit

Permalink
web-client: Add basic Merkle Tree and Path APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
sisou committed Mar 1, 2024
1 parent a1037b2 commit 2e69acf
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 19 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion primitives/transaction/src/signature_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ impl SignatureProof {

pub fn try_from_webauthn(
public_key: PublicKey,
merkle_path: Option<Blake2bMerklePath>,
signature: Signature,
authenticator_data: &[u8],
client_data_json: &[u8],
Expand Down Expand Up @@ -116,7 +117,7 @@ impl SignatureProof {

Ok(SignatureProof {
public_key,
merkle_path: Blake2bMerklePath::empty(),
merkle_path: merkle_path.unwrap_or_default(),
signature,
webauthn_fields: Some(WebauthnExtraFields {
host,
Expand Down
1 change: 1 addition & 0 deletions web-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ nimiq-primitives = { workspace = true, features = ["coin", "networks", "ts-types
nimiq-serde = { workspace = true }
nimiq-transaction = { workspace = true, features = ["ts-types"] }
nimiq-transaction-builder = { workspace = true }
nimiq-utils = { workspace = true, features = ["merkle"] }

[dependencies.nimiq]
package = "nimiq-lib"
Expand Down
42 changes: 42 additions & 0 deletions web-client/src/primitives/merkle_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use js_sys::Uint8Array;
use nimiq_serde::Serialize;
use nimiq_utils::merkle::compute_root_from_content;
use wasm_bindgen::prelude::wasm_bindgen;

/// The Merkle tree is a data structure that allows for efficient verification of the membership of an element in a set.
#[wasm_bindgen]
pub struct MerkleTree;

#[wasm_bindgen]
impl MerkleTree {
/// Computes the root of a Merkle tree from a list of Uint8Arrays.
#[wasm_bindgen(js_name = computeRoot)]
pub fn compute_root(values: Vec<Uint8Array>) -> Vec<u8> {
let mut values: Vec<_> = values.iter().map(|u| u.to_vec()).collect();
values.sort_unstable();

let root = compute_root_from_content::<nimiq_hash::Blake2bHasher, Vec<u8>>(&values);
root.serialize_to_vec()
}
}

#[cfg(test)]
mod tests {
use js_sys::Uint8Array;
use wasm_bindgen_test::wasm_bindgen_test;

use crate::primitives::merkle_tree::MerkleTree;

#[wasm_bindgen_test]
pub fn it_computes_the_same_root_for_any_array_order() {
let value1 = Uint8Array::new_with_length(8);
value1.copy_from(&[1, 2, 3, 4, 5, 6, 7, 8]);
let value2 = Uint8Array::new_with_length(8);
value2.copy_from(&[9, 10, 11, 12, 13, 14, 15, 16]);

let root_ordered = MerkleTree::compute_root(vec![value1.clone(), value2.clone()]);
let root_unordered = MerkleTree::compute_root(vec![value2.clone(), value1.clone()]);

assert_eq!(root_ordered, root_unordered);
}
}
1 change: 1 addition & 0 deletions web-client/src/primitives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod es256_public_key;
pub mod es256_signature;
pub mod hash;
pub mod key_pair;
pub mod merkle_tree;
pub mod private_key;
pub mod public_key;
pub mod signature;
Expand Down
142 changes: 124 additions & 18 deletions web-client/src/primitives/signature_proof.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use js_sys::Array;
use nimiq_serde::Serialize;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -27,35 +28,88 @@ impl SignatureProof {
))
}

/// Creates a ES256/Webauthn signature proof for a single-sig signature.
/// Creates a Ed25519/Schnorr signature proof for a multi-sig signature.
/// The public keys can also include ES256 keys.
#[wasm_bindgen(js_name = multiSig)]
pub fn multi_sig(
signer_key: &PublicKey,
public_keys: &PublicKeyUnionArray,
signature: &Signature,
) -> Result<SignatureProof, JsError> {
let mut public_keys: Vec<_> = SignatureProof::unpack_public_keys(public_keys)?
.into_iter()
.map(|k| match k {
nimiq_keys::PublicKey::Ed25519(ref public_key) => public_key.serialize_to_vec(),
nimiq_keys::PublicKey::ES256(ref public_key) => public_key.serialize_to_vec(),
})
.collect();
public_keys.sort_unstable();

let merkle_path = nimiq_utils::merkle::Blake2bMerklePath::new::<
nimiq_hash::Blake2bHasher,
Vec<u8>,
>(&public_keys, &signer_key.native_ref().serialize_to_vec());

Ok(SignatureProof::from(nimiq_transaction::SignatureProof {
public_key: nimiq_keys::PublicKey::Ed25519(*signer_key.native_ref()),
merkle_path,
signature: nimiq_keys::Signature::Ed25519(signature.native_ref().clone()),
webauthn_fields: None,
}))
}

/// Creates a Webauthn signature proof for a single-sig signature.
#[wasm_bindgen(js_name = webauthnSingleSig)]
pub fn webauthn_single_sig(
public_key: &PublicKeyUnion,
signature: &SignatureUnion,
authenticator_data: &[u8],
client_data_json: &[u8],
) -> Result<SignatureProof, JsError> {
let js_value: &JsValue = public_key.unchecked_ref();
let public_key = if let Ok(key) = PublicKey::try_from(js_value) {
nimiq_keys::PublicKey::Ed25519(*key.native_ref())
} else if let Ok(key) = ES256PublicKey::try_from(js_value) {
nimiq_keys::PublicKey::ES256(*key.native_ref())
} else {
return Err(JsError::new("Invalid public key"));
};

let js_value: &JsValue = signature.unchecked_ref();
let signature = if let Ok(sig) = Signature::try_from(js_value) {
nimiq_keys::Signature::Ed25519(sig.native_ref().clone())
} else if let Ok(sig) = ES256Signature::try_from(js_value) {
nimiq_keys::Signature::ES256(sig.native_ref().clone())
} else {
return Err(JsError::new("Invalid signature"));
};
let public_key = SignatureProof::unpack_public_key(public_key)?;
let signature = SignatureProof::unpack_signature(signature)?;

Ok(SignatureProof::from(
nimiq_transaction::SignatureProof::try_from_webauthn(
public_key,
None,
signature,
authenticator_data,
client_data_json,
)?,
))
}

/// Creates a Webauthn signature proof for a multi-sig signature.
#[wasm_bindgen(js_name = webauthnMultiSig)]
pub fn webauthn_multi_sig(
signer_key: &PublicKeyUnion,
public_keys: &PublicKeyUnionArray,
signature: &SignatureUnion,
authenticator_data: &[u8],
client_data_json: &[u8],
) -> Result<SignatureProof, JsError> {
let signer_key = SignatureProof::unpack_public_key(signer_key)?;
let signature = SignatureProof::unpack_signature(signature)?;

let mut public_keys: Vec<_> = SignatureProof::unpack_public_keys(public_keys)?
.into_iter()
.map(|k| match k {
nimiq_keys::PublicKey::Ed25519(ref public_key) => public_key.serialize_to_vec(),
nimiq_keys::PublicKey::ES256(ref public_key) => public_key.serialize_to_vec(),
})
.collect();
public_keys.sort_unstable();

let merkle_path = nimiq_utils::merkle::Blake2bMerklePath::new::<
nimiq_hash::Blake2bHasher,
Vec<u8>,
>(&public_keys, &signer_key.serialize_to_vec());

Ok(SignatureProof::from(
nimiq_transaction::SignatureProof::try_from_webauthn(
signer_key,
Some(merkle_path),
signature,
authenticator_data,
client_data_json,
Expand Down Expand Up @@ -122,13 +176,65 @@ impl SignatureProof {
pub fn native_ref(&self) -> &nimiq_transaction::SignatureProof {
&self.inner
}

fn unpack_public_keys(
public_keys: &PublicKeyUnionArray,
) -> Result<Vec<nimiq_keys::PublicKey>, JsError> {
// Unpack the array of public keys
let js_value: &JsValue = public_keys.unchecked_ref();
let array: &Array = js_value
.dyn_ref()
.ok_or_else(|| JsError::new("`public_keys` must be an array"))?;

if array.length() == 0 {
return Err(JsError::new("No public keys provided"));
}

let mut public_keys = Vec::<_>::with_capacity(array.length().try_into()?);
for item in array.iter() {
let public_key = SignatureProof::unpack_public_key(item.unchecked_ref())
.map_err(|_| JsError::new("Invalid public key in array"))?;
public_keys.push(public_key);
}

Ok(public_keys)
}

fn unpack_public_key(public_key: &PublicKeyUnion) -> Result<nimiq_keys::PublicKey, JsError> {
let js_value: &JsValue = public_key.unchecked_ref();
let public_key = if let Ok(key) = PublicKey::try_from(js_value) {
nimiq_keys::PublicKey::Ed25519(*key.native_ref())
} else if let Ok(key) = ES256PublicKey::try_from(js_value) {
nimiq_keys::PublicKey::ES256(*key.native_ref())
} else {
return Err(JsError::new("Invalid public key"));
};

Ok(public_key)
}

fn unpack_signature(signature: &SignatureUnion) -> Result<nimiq_keys::Signature, JsError> {
let js_value: &JsValue = signature.unchecked_ref();
let signature = if let Ok(sig) = Signature::try_from(js_value) {
nimiq_keys::Signature::Ed25519(sig.native_ref().clone())
} else if let Ok(sig) = ES256Signature::try_from(js_value) {
nimiq_keys::Signature::ES256(sig.native_ref().clone())
} else {
return Err(JsError::new("Invalid signature"));
};

Ok(signature)
}
}

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "PublicKey | ES256PublicKey")]
pub type PublicKeyUnion;

#[wasm_bindgen(typescript_type = "(PublicKey | ES256PublicKey)[]")]
pub type PublicKeyUnionArray;

#[wasm_bindgen(typescript_type = "Signature | ES256Signature")]
pub type SignatureUnion;
}
Expand Down

0 comments on commit 2e69acf

Please sign in to comment.