diff --git a/README.md b/README.md index 7ad1e90..663cb63 100644 --- a/README.md +++ b/README.md @@ -134,13 +134,6 @@ cargo run --example split --features http # mint a coin, split it, verify o A self-contained demo application is provided under [`e2e/`](./e2e). -## Regenerating cross-SDK fixtures - -```sh -cd ../state-transition-sdk-js && npm install && npm run build -node generate-fixtures.mjs > tests/vectors/transition_flow.json -``` - ## License MIT OR Apache-2.0. diff --git a/e2e/Cargo.lock b/e2e/Cargo.lock index 3d4998e..f2d5e4f 100644 --- a/e2e/Cargo.lock +++ b/e2e/Cargo.lock @@ -8,6 +8,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "base16ct" version = "0.2.0" @@ -408,6 +414,34 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -687,9 +721,13 @@ dependencies = [ "getrandom", "hex", "k256", + "num-bigint", + "num-traits", "serde_json", "sha2", "ureq", + "url", + "zeroize", ] [[package]] diff --git a/src/api/inclusion_certificate.rs b/src/api/inclusion_certificate.rs index d1aea72..a7e9133 100644 --- a/src/api/inclusion_certificate.rs +++ b/src/api/inclusion_certificate.rs @@ -2,7 +2,7 @@ //! `(stateId -> transactionHash)` leaf is committed under a block's state root. //! //! Wire form (not CBOR-tagged): a 32-byte bitmap followed by the sibling hashes -//! (32 bytes each). The bitmap's set bits — one per tree depth, LSB-first — +//! (32 bytes each). The bitmap's set bits — one per tree depth, MSB-first — //! select which depths contribute a sibling; their count must equal the number //! of sibling hashes. @@ -14,9 +14,8 @@ use crate::error::Error; const BITMAP_SIZE: usize = 32; const HASH_SIZE: usize = 32; -const MAX_DEPTH: usize = 255; -use crate::radix::bit_at; +use crate::radix::{bit_at, prefix_region, MAX_DEPTH}; /// A sparse-Merkle-tree inclusion path. #[derive(Debug, Clone, PartialEq, Eq)] @@ -94,10 +93,12 @@ impl InclusionCertificate { } position -= 1; let sibling = &self.siblings[position]; + let region = prefix_region(key, depth); let h = DataHasher::new(HashAlgorithm::Sha256) .expect("sha256") - .update(&[0x01, depth as u8]); + .update(&[0x01, depth as u8]) + .update(®ion); hash = if bit_at(key, depth) { h.update(sibling).update(hash.data()) } else { @@ -109,3 +110,95 @@ impl InclusionCertificate { position == 0 && &hash == expected_root } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::cbor::{encode_byte_string, Decoder}; + + fn data_hash(bytes: [u8; 32]) -> DataHash { + DataHash::new(HashAlgorithm::Sha256, bytes).unwrap() + } + + fn state_id(bytes: [u8; 32]) -> StateId { + StateId::from_cbor(Decoder::new(&encode_byte_string(&bytes))).unwrap() + } + + fn leaf_hash(key: &[u8; 32], value: &[u8; 32]) -> DataHash { + DataHasher::new(HashAlgorithm::Sha256) + .expect("sha256") + .update(&[0x00]) + .update(key) + .update(value) + .finalize() + } + + fn old_3o_node_hash(depth: usize, left: &DataHash, right: &DataHash) -> DataHash { + DataHasher::new(HashAlgorithm::Sha256) + .expect("sha256") + .update(&[0x01, depth as u8]) + .update(left.data()) + .update(right.data()) + .finalize() + } + + fn v6a_node_hash(key: &[u8; 32], depth: usize, left: &DataHash, right: &DataHash) -> DataHash { + let region = prefix_region(key, depth); + DataHasher::new(HashAlgorithm::Sha256) + .expect("sha256") + .update(&[0x01, depth as u8]) + .update(®ion) + .update(left.data()) + .update(right.data()) + .finalize() + } + + #[test] + fn two_leaf_inclusion_uses_v6a_region_commitment() { + let left_key = [0u8; 32]; + let mut right_key = [0u8; 32]; + right_key[0] = 0b1000_0000; // diverges at depth 0 in MSB-first order + + let left_value = [0x11u8; 32]; + let right_value = [0x22u8; 32]; + let left_leaf = leaf_hash(&left_key, &left_value); + let right_leaf = leaf_hash(&right_key, &right_value); + + let root = v6a_node_hash(&left_key, 0, &left_leaf, &right_leaf); + let old_root = old_3o_node_hash(0, &left_leaf, &right_leaf); + + let mut encoded = [0u8; BITMAP_SIZE + HASH_SIZE]; + encoded[0] = 0b1000_0000; // sibling at depth 0 + encoded[BITMAP_SIZE..].copy_from_slice(right_leaf.data()); + let proof = InclusionCertificate::decode(&encoded).unwrap(); + + assert!(proof.verify(&state_id(left_key), &data_hash(left_value), &root)); + assert!(!proof.verify(&state_id(left_key), &data_hash(left_value), &old_root)); + } + + #[test] + fn deep_split_region_is_derived_from_key_prefix() { + let mut left_key = [0u8; 32]; + left_key[0] = 0b1010_1101; + left_key[1] = 0b1100_0011; + + let mut right_key = left_key; + right_key[1] ^= 0b0010_0000; // diverges at depth 10 + + let left_value = [0x33u8; 32]; + let right_value = [0x44u8; 32]; + let left_leaf = leaf_hash(&left_key, &left_value); + let right_leaf = leaf_hash(&right_key, &right_value); + + let root = v6a_node_hash(&left_key, 10, &left_leaf, &right_leaf); + let old_root = old_3o_node_hash(10, &left_leaf, &right_leaf); + + let mut encoded = [0u8; BITMAP_SIZE + HASH_SIZE]; + encoded[1] = 0b0010_0000; // sibling at depth 10 + encoded[BITMAP_SIZE..].copy_from_slice(right_leaf.data()); + let proof = InclusionCertificate::decode(&encoded).unwrap(); + + assert!(proof.verify(&state_id(left_key), &data_hash(left_value), &root)); + assert!(!proof.verify(&state_id(left_key), &data_hash(left_value), &old_root)); + } +} diff --git a/src/payment/asset.rs b/src/payment/asset.rs index f8fda33..d003d5e 100644 --- a/src/payment/asset.rs +++ b/src/payment/asset.rs @@ -106,7 +106,10 @@ impl Asset { /// Encode to CBOR: `[bstr(id), bstr(value)]`. fn to_cbor(&self) -> Vec { - encode_array(&[&self.id.to_cbor(), &encode_byte_string(&encode_amount(&self.value))]) + encode_array(&[ + &self.id.to_cbor(), + &encode_byte_string(&encode_amount(&self.value)), + ]) } } diff --git a/src/payment/commitment.rs b/src/payment/commitment.rs index c286538..bcb949c 100644 --- a/src/payment/commitment.rs +++ b/src/payment/commitment.rs @@ -59,9 +59,9 @@ pub fn split_output_commitment( /// Errors if the output carries no auxiliary payload (a split output must always /// declare the non-empty canonical asset collection it receives). pub fn commitment_for_mint(source_id: &TokenId, mint: &MintTransaction) -> Result<[u8; 32], Error> { - let aux_data = mint - .data() - .ok_or(Error::UnexpectedValue("split output has no auxiliary payload"))?; + let aux_data = mint.data().ok_or(Error::UnexpectedValue( + "split output has no auxiliary payload", + ))?; Ok(split_output_commitment( source_id, mint.network_id(), diff --git a/src/payment/justification.rs b/src/payment/justification.rs index 0b20433..3c59cad 100644 --- a/src/payment/justification.rs +++ b/src/payment/justification.rs @@ -92,8 +92,11 @@ impl SplitMintJustification { /// Encode to CBOR (tag 39044): `[token, [proofs...]]`. pub fn to_cbor(&self) -> Vec { - let proof_bytes: Vec> = - self.proofs.iter().map(RsmstInclusionProof::to_cbor).collect(); + let proof_bytes: Vec> = self + .proofs + .iter() + .map(RsmstInclusionProof::to_cbor) + .collect(); let proof_refs: Vec<&[u8]> = proof_bytes.iter().map(Vec::as_slice).collect(); encode_tag( SPLIT_MINT_JUSTIFICATION_TAG, diff --git a/src/payment/split.rs b/src/payment/split.rs index 36f0678..8824019 100644 --- a/src/payment/split.rs +++ b/src/payment/split.rs @@ -270,9 +270,9 @@ impl TokenSplit { .iter() .find(|(id, _)| id == asset.id()) .expect("a tree is built for every source asset"); - let proof = built - .proof(token_id.bytes()) - .ok_or(Error::UnexpectedValue("missing allocation proof for output"))?; + let proof = built.proof(token_id.bytes()).ok_or(Error::UnexpectedValue( + "missing allocation proof for output", + ))?; proofs.push(proof); } tokens.push(SplitToken { diff --git a/src/payment/verifier.rs b/src/payment/verifier.rs index e4957cc..4ae1328 100644 --- a/src/payment/verifier.rs +++ b/src/payment/verifier.rs @@ -214,9 +214,8 @@ impl MintJustificationVerifier for SplitMintJustificationVerifier { } let manifest = SplitManifest::from_cbor_bytes(manifest_bytes, limits) .map_err(|_| VerificationError::SplitManifestMalformed)?; - let expected_burn = EncodedPredicate::from_predicate(&BurnPredicate::new( - manifest.reason_hash().to_vec(), - )); + let expected_burn = + EncodedPredicate::from_predicate(&BurnPredicate::new(manifest.reason_hash().to_vec())); if burn_transfer.recipient() != &expected_burn { return Err(VerificationError::SplitBurnPredicateMismatch); } diff --git a/src/radix.rs b/src/radix.rs index a8e2078..7a577dd 100644 --- a/src/radix.rs +++ b/src/radix.rs @@ -6,10 +6,156 @@ //! certificate bitmap) in the same order, so they share this single helper — //! there is exactly one key/depth convention in the SDK. //! -//! Bit `i` is bit `i % 8` of byte `i / 8`: least significant bit first within -//! each byte, byte 0 first. +//! Bit `i` is the `(i % 8)`-th most-significant bit of byte `i / 8`. -/// LSB-first bit `index` of a byte string: bit `index % 8` of byte `index / 8`. +pub(crate) const KEY_BITS: usize = 256; +pub(crate) const MAX_DEPTH: usize = KEY_BITS - 1; + +/// MSB-first bit `index` of `data`: the `(index % 8)`-th most-significant bit +/// of byte `index / 8`. +#[inline] pub(crate) fn bit_at(data: &[u8], index: usize) -> bool { - (data[index / 8] >> (index % 8)) & 1 == 1 + assert!(index < data.len() * 8, "bit index out of range"); + let byte_index = index / 8; + let bit_in_byte = index % 8; + data[byte_index] & (0x80 >> bit_in_byte) != 0 +} + +/// Canonical radix region for an internal node at `depth`. +/// +/// The SDK's radix order is MSB-first within each byte, byte 0 first. The +/// region therefore keeps key bits `0..depth` and clears the split bit and all +/// later bits. For example, `depth == 0` is the empty prefix, and +/// `depth == 255` keeps bits `0..254` while clearing bit 255. +pub(crate) fn prefix_region(key: &[u8; 32], depth: usize) -> [u8; 32] { + assert!(depth <= MAX_DEPTH, "depth cannot exceed MAX_DEPTH"); + + let mut region = [0u8; 32]; + let full_bytes = depth / 8; + let prefix_bits = depth % 8; + + region[..full_bytes].copy_from_slice(&key[..full_bytes]); + if prefix_bits != 0 { + let mask = 0xffu8 << (8 - prefix_bits); + region[full_bytes] = key[full_bytes] & mask; + } + + region +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bit_at_uses_msb_first_radix_order() { + let key = [ + 0b1000_0001, + 0b1000_0000, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0b0000_0001, + ]; + + assert!(bit_at(&key, 0)); + assert!(!bit_at(&key, 1)); + assert!(!bit_at(&key, 6)); + assert!(bit_at(&key, 7)); + assert!(bit_at(&key, 8)); + assert!(bit_at(&key, 255)); + } + + #[test] + fn prefix_region_uses_shared_msb_first_radix_order() { + let key = [ + 0b1010_1101, + 0b1100_0011, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0b1000_0001, + ]; + + assert_eq!(prefix_region(&key, 0), [0u8; 32]); + + let mut depth_3 = [0u8; 32]; + depth_3[0] = 0b1010_0000; + assert_eq!(prefix_region(&key, 3), depth_3); + + let mut depth_9 = [0u8; 32]; + depth_9[0] = key[0]; + depth_9[1] = 0b1000_0000; + assert_eq!(prefix_region(&key, 9), depth_9); + + let mut depth_255 = key; + depth_255[31] &= 0xfe; + assert_eq!(prefix_region(&key, 255), depth_255); + } + + #[test] + #[should_panic(expected = "depth cannot exceed MAX_DEPTH")] + fn prefix_region_rejects_leaf_depth() { + let key = [0u8; 32]; + let _ = prefix_region(&key, KEY_BITS); + } + + #[test] + #[should_panic(expected = "bit index out of range")] + fn bit_at_rejects_out_of_range_index() { + let key = [0u8; 32]; + let _ = bit_at(&key, KEY_BITS); + } } diff --git a/src/rsmst/build.rs b/src/rsmst/build.rs index 7fe3f36..496e842 100644 --- a/src/rsmst/build.rs +++ b/src/rsmst/build.rs @@ -192,17 +192,18 @@ impl BuiltRsmst { }); } Node::Branch { - depth, - left, - right, - .. + depth, left, right, .. } => { let (sibling, next): (&Node, &Node) = if key_bit(key, *depth) { (left, right) } else { (right, left) }; - steps.push(RsmstProofStep::new(*depth, *sibling.hash(), sibling.sum().clone())); + steps.push(RsmstProofStep::new( + *depth, + *sibling.hash(), + sibling.sum().clone(), + )); node = next; } } @@ -230,7 +231,10 @@ mod tests { tree.insert(k, d, BigUint::from(42u32)).unwrap(); let built = tree.build().unwrap(); assert_eq!(*built.root_sum(), BigUint::from(42u32)); - assert_eq!(built.root_hash(), leaf_hash(&k, &d, &BigUint::from(42u32)).unwrap()); + assert_eq!( + built.root_hash(), + leaf_hash(&k, &d, &BigUint::from(42u32)).unwrap() + ); let proof = built.proof(&k).unwrap(); assert!(proof.steps().is_empty()); assert_eq!( @@ -278,7 +282,9 @@ mod tests { let k = key(&[(31, 1)]); tree.insert(k, [0; 32], BigUint::from(1u32)).unwrap(); assert!(tree.insert(k, [1; 32], BigUint::from(2u32)).is_err()); - assert!(tree.insert(key(&[(31, 2)]), [0; 32], BigUint::ZERO).is_err()); + assert!(tree + .insert(key(&[(31, 2)]), [0; 32], BigUint::ZERO) + .is_err()); } /// Golden cross-implementation vector. The digests below were computed @@ -286,9 +292,9 @@ mod tests { /// (`SHA-256(0x10 || k || d || u256(v))` for leaves, /// `SHA-256(0x11 || depth || hL || u256(vL) || hR || u256(vR))` for nodes) /// using the SDK's shared bit order ([`crate::radix::bit_at`]). The keys - /// `0x00..01` (byte 31 = 1) and `0x00..00` first differ at bit 248 (the low - /// bit of byte 31), so the root bifurcates at depth 248 with the all-zero key - /// (bit 248 = 0) on the left. + /// `0x00..01` (byte 31 = 1) and `0x00..00` first differ at bit 255 (the low + /// bit of byte 31), so the root bifurcates at depth 255 with the all-zero key + /// (bit 255 = 0) on the left. #[test] fn golden_two_leaf_root() { use hex_literal::hex; @@ -312,14 +318,14 @@ mod tests { let built = tree.build().unwrap(); assert_eq!( built.root_hash(), - hex!("8edbd116cc67ca16dfd3b7852a11bdbc7048d446c5ce7f112d6476c28c6b5a96") + hex!("42e8d38b002c192bb7a6f121692f55b46626f68b6e9ca59b7f638aecfb4632b1") ); assert_eq!(*built.root_sum(), BigUint::from(30u32)); - // The single-step proof for key_a folds in sibling key_b at depth 248. + // The single-step proof for key_a folds in sibling key_b at depth 255. let proof = built.proof(&key_a).unwrap(); assert_eq!(proof.steps().len(), 1); - assert_eq!(proof.steps()[0].depth(), 248); + assert_eq!(proof.steps()[0].depth(), 255); assert_eq!( proof.verify(&key_a, &data_a, &BigUint::from(10u32), &built.root_hash()), Some(BigUint::from(30u32)) diff --git a/src/rsmst/mod.rs b/src/rsmst/mod.rs index 83a11f9..a45ec64 100644 --- a/src/rsmst/mod.rs +++ b/src/rsmst/mod.rs @@ -1,7 +1,7 @@ //! Radix Sparse Merkle Sum Tree (RSMST). //! //! An RSMST is the radix sparse Merkle tree (a leaf-anchored, path-compressed -//! binary trie over a 256-bit key space, LSB-first) decorated with a positive +//! binary trie over a 256-bit key space, MSB-first) decorated with a positive //! amount at every leaf and an accumulated sum at every internal node. It backs //! the per-asset split-allocation roots of the token-splitting protocol //! (yellowpaper Appendix "Radix Sparse Merkle Sum Trees"): one tree per asset @@ -150,9 +150,9 @@ pub fn node_hash( } /// Bit `index` of a 256-bit key, using the SDK's one shared radix bit order -/// ([`crate::radix::bit_at`]): bit `index % 8` of byte `index / 8`. The builder -/// and the verifier go through this same helper, so they share exactly one -/// key/depth convention with the transaction inclusion certificate. +/// ([`crate::radix::bit_at`]). The builder and the verifier go through this +/// same helper, so they share exactly one key/depth convention with the +/// transaction inclusion certificate. fn key_bit(key: &[u8; 32], index: u8) -> bool { crate::radix::bit_at(key, index as usize) } @@ -163,11 +163,11 @@ mod tests { #[test] fn key_bit_matches_shared_radix_convention() { - // Bit i = bit (i%8) of byte (i/8), byte 0 first (the shared RSMT order). + // Bit i is the (i % 8)-th most-significant bit of byte i / 8. let mut key = [0u8; 32]; - key[0] = 0b0000_0101; // bits 0 and 2 set - key[1] = 0b0000_0010; // bit 9 set - key[31] = 0b1000_0000; // bit 255 set + key[0] = 0b1010_0000; // bits 0 and 2 set + key[1] = 0b0100_0000; // bit 9 set + key[31] = 0b0000_0001; // bit 255 set assert!(key_bit(&key, 0)); assert!(!key_bit(&key, 1)); assert!(key_bit(&key, 2)); diff --git a/src/rsmst/proof.rs b/src/rsmst/proof.rs index 3b5ec14..efd06af 100644 --- a/src/rsmst/proof.rs +++ b/src/rsmst/proof.rs @@ -57,14 +57,15 @@ impl RsmstProofStep { let items = d.array(Some(3))?; let depth = u8::try_from(items[0].uint()?) .map_err(|_| Error::OutOfRange("RSMST proof depth exceeds 255"))?; - let hash: [u8; 32] = items[1] - .bytes_value()? - .try_into() - .map_err(|_| Error::InvalidLength { - what: "RSMST sibling hash", - expected: 32, - actual: 0, - })?; + let hash: [u8; 32] = + items[1] + .bytes_value()? + .try_into() + .map_err(|_| Error::InvalidLength { + what: "RSMST sibling hash", + expected: 32, + actual: 0, + })?; let sum = decode_positive_amount(items[2].bytes_value()?)?; Ok(RsmstProofStep { depth, hash, sum }) } diff --git a/src/verify/error.rs b/src/verify/error.rs index a2d8dbe..f9e7a9e 100644 --- a/src/verify/error.rs +++ b/src/verify/error.rs @@ -149,10 +149,16 @@ impl fmt::Display for VerificationError { write!(f, "split asset is absent from burned source token") } VerificationError::SplitSourceAmountMismatch => { - write!(f, "reconstructed root sum does not match burned source amount") + write!( + f, + "reconstructed root sum does not match burned source amount" + ) } VerificationError::SplitBurnPredicateMismatch => { - write!(f, "burned token not locked to split manifest burn predicate") + write!( + f, + "burned token not locked to split manifest burn predicate" + ) } VerificationError::InclusionCertificateMissing => { write!(f, "inclusion certificate missing") diff --git a/tests/transition_flow.rs b/tests/transition_flow.rs index b11c0a0..0acb3f6 100644 --- a/tests/transition_flow.rs +++ b/tests/transition_flow.rs @@ -1,11 +1,13 @@ //! End-to-end cross-SDK verification test. //! //! `tests/vectors/transition_flow.json` is produced by the reference -//! TypeScript SDK (see `state-transition-sdk-js/generate-fixtures.mjs`): a token -//! minted to Alice and transferred Alice -> Bob -> Carol through an in-memory -//! aggregator, with the JS SDK asserting `verify == OK` before export. This test +//! TypeScript SDK (`tests/functional/TestAggregatorClient.ts` + +//! `tests/utils/TokenUtils.ts`): a token minted to Alice and transferred +//! Alice -> Bob -> Carol through an in-memory aggregator, with the JS SDK +//! asserting `verify == OK` after every step before export. This test //! confirms the Rust SDK decodes those exact bytes, round-trips them -//! byte-for-byte, and reaches the same verification decisions. +//! byte-for-byte, and reaches the same verification decisions (RSMT v6a, +//! big-endian bit order). use unicity_token::api::bft::root_trust_base::RootTrustBaseNodeInfo; use unicity_token::api::bft::RootTrustBase; diff --git a/tests/vectors/transition_flow.json b/tests/vectors/transition_flow.json index 9af2b14..645f881 100644 --- a/tests/vectors/transition_flow.json +++ b/tests/vectors/transition_flow.json @@ -1,11 +1,12 @@ { - "trustBase": { - "networkId": 3, - "nodeId": "NODE", - "aggregatorPublicKey": "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa", - "quorumThreshold": 1 - }, - "aliceToken": "d99880830182d99881870103d99878830141015821028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f7582055555555555555555555555555555555555555555555555555555555555555555820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaf6f6d998798401d998778501d9987883014101582102275234b05a0cb83d1b4b405007de6d9c836fc4d6c16fbe0b4e05d2a13444101658205d73e630fc26304b31f26b521987d23800b1c8d6f963e8746fe4d0bd2761074458202dea4b946b1282e1146d34430aaa06f1b97f52e325386a20e02c9d59b254300e584124522456caf746979059f31e3295bdad5b2c690a8d23eb8f18d6e604510ac2f54b3790392b9e21662aa5696174e2a78448fe44c829474dbf6e73ee4f98ed9c9b0058200000000000000000000000000000000000000000000000000000000000000000d998598701d9985a8a010000f658207e68d224f1cce2fa31d422f889187c4a8989e2f49cd7cd202b3ba353a1c15ef14000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f6582081d89ab7dd5c6dd53b80bb5de706882886bee7f6a023acf13cf632034017695da1644e4f44455841ba7a76b2bc27856fab46d4188b44fd3cebbd676bf21eea0979753ff717fc2cfd1fc08407f86f47ed137fc3bd8fae7edbb7a8bb1e6418e5147f515d09fcf419bd0080", - "bobToken": "d99880830182d99881870103d99878830141015821028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f7582055555555555555555555555555555555555555555555555555555555555555555820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaf6f6d998798401d998778501d9987883014101582102275234b05a0cb83d1b4b405007de6d9c836fc4d6c16fbe0b4e05d2a13444101658205d73e630fc26304b31f26b521987d23800b1c8d6f963e8746fe4d0bd2761074458202dea4b946b1282e1146d34430aaa06f1b97f52e325386a20e02c9d59b254300e584124522456caf746979059f31e3295bdad5b2c690a8d23eb8f18d6e604510ac2f54b3790392b9e21662aa5696174e2a78448fe44c829474dbf6e73ee4f98ed9c9b0058200000000000000000000000000000000000000000000000000000000000000000d998598701d9985a8a010000f658207e68d224f1cce2fa31d422f889187c4a8989e2f49cd7cd202b3ba353a1c15ef14000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f6582081d89ab7dd5c6dd53b80bb5de706882886bee7f6a023acf13cf632034017695da1644e4f44455841ba7a76b2bc27856fab46d4188b44fd3cebbd676bf21eea0979753ff717fc2cfd1fc08407f86f47ed137fc3bd8fae7edbb7a8bb1e6418e5147f515d09fcf419bd008182d998858401d99878830141015821036930f46dd0b16d866d59d1054aa63298b357499cd1862ef16f3f55f1cafceb8258206666666666666666666666666666666666666666666666666666666666666666f6d998798401d998778501d99878830141015821028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f758203e943be5f92487fc6dd3b0f18d13d3c46e386da47a3af9c4ea36d389791cda7a5820905e537d2a43e3a7c9e39a529cac8f2b3ac68140c8fc799276346b42d275e2eb5841f7fb919aaeb6b863eacee5657e6ed57246943b24bf61981e6332166160bf7ee0619cec434f8ff6c6de1797853a7bfeeaf28c98242f3f86b2ffe4ffa5ab98037c01584002000000000000000000000000000000000000000000000000000000000000007e68d224f1cce2fa31d422f889187c4a8989e2f49cd7cd202b3ba353a1c15ef1d998598701d9985a8a010000f658202a28c6564f1a7e1a40fc2ee7401115cabdecd9bccab298427b65e6df91c58e214000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f65820cc297650a5249be63d4ca8cc2e384e35b65326a3cefca90240d6945987340314a1644e4f44455841d892a710a873256d71d155e39813b897247028cb5c3deb595fb548acb12c9de273f639d7096bbc2f7d379810bdf368a2e86df83faa0322f4a1ff46bcc5c1448101", - "carolToken": "d99880830182d99881870103d99878830141015821028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f7582055555555555555555555555555555555555555555555555555555555555555555820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaf6f6d998798401d998778501d9987883014101582102275234b05a0cb83d1b4b405007de6d9c836fc4d6c16fbe0b4e05d2a13444101658205d73e630fc26304b31f26b521987d23800b1c8d6f963e8746fe4d0bd2761074458202dea4b946b1282e1146d34430aaa06f1b97f52e325386a20e02c9d59b254300e584124522456caf746979059f31e3295bdad5b2c690a8d23eb8f18d6e604510ac2f54b3790392b9e21662aa5696174e2a78448fe44c829474dbf6e73ee4f98ed9c9b0058200000000000000000000000000000000000000000000000000000000000000000d998598701d9985a8a010000f658207e68d224f1cce2fa31d422f889187c4a8989e2f49cd7cd202b3ba353a1c15ef14000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f6582081d89ab7dd5c6dd53b80bb5de706882886bee7f6a023acf13cf632034017695da1644e4f44455841ba7a76b2bc27856fab46d4188b44fd3cebbd676bf21eea0979753ff717fc2cfd1fc08407f86f47ed137fc3bd8fae7edbb7a8bb1e6418e5147f515d09fcf419bd008282d998858401d99878830141015821036930f46dd0b16d866d59d1054aa63298b357499cd1862ef16f3f55f1cafceb8258206666666666666666666666666666666666666666666666666666666666666666f6d998798401d998778501d99878830141015821028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f758203e943be5f92487fc6dd3b0f18d13d3c46e386da47a3af9c4ea36d389791cda7a5820905e537d2a43e3a7c9e39a529cac8f2b3ac68140c8fc799276346b42d275e2eb5841f7fb919aaeb6b863eacee5657e6ed57246943b24bf61981e6332166160bf7ee0619cec434f8ff6c6de1797853a7bfeeaf28c98242f3f86b2ffe4ffa5ab98037c01584002000000000000000000000000000000000000000000000000000000000000007e68d224f1cce2fa31d422f889187c4a8989e2f49cd7cd202b3ba353a1c15ef1d998598701d9985a8a010000f658202a28c6564f1a7e1a40fc2ee7401115cabdecd9bccab298427b65e6df91c58e214000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f65820cc297650a5249be63d4ca8cc2e384e35b65326a3cefca90240d6945987340314a1644e4f44455841d892a710a873256d71d155e39813b897247028cb5c3deb595fb548acb12c9de273f639d7096bbc2f7d379810bdf368a2e86df83faa0322f4a1ff46bcc5c144810182d998858401d9987883014101582102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661958207777777777777777777777777777777777777777777777777777777777777777f6d998798401d998778501d99878830141015821036930f46dd0b16d866d59d1054aa63298b357499cd1862ef16f3f55f1cafceb8258200a03370c89457d674c68f89e2cea4c7d71d370315c8879d33921129c3c11014358206a93f3b133d5c66fc3af88f47d14032bf593a0a62b975be536d5696f77ce243158414f1a2be8f08fac82162bce2e01683b966e4d88f8ce535d5b620249d2c2e733b87825eacbe5016eee6d8e93048395f44bc6f8c36e0d5e21dd9befbef2a6cd1a6201584001000000000000000000000000000000000000000000000000000000000000002a28c6564f1a7e1a40fc2ee7401115cabdecd9bccab298427b65e6df91c58e21d998598701d9985a8a010000f65820e78a66ff006ccc709e9ffb2a3e18d700863d51756bb13c02b12e236b006c09cb4000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f658209bf8fc64476a3dc31558581603d6fb5c9806a51f8449f5b2c08413f936bcea92a1644e4f44455841ad1cedec5ce3ba823e7a540d7422f0169a0d64c55a8f7e27043c3af683a704923b5b54e4514fcef1bda1e29183d369f56f156edd0e5642af9c50a342cbfb457900" -} \ No newline at end of file + "__comment": "generated by state-transition-sdk-js f4cc9056375759844fc1217289d765948dbbd88b", + "trustBase": { + "networkId": 3, + "nodeId": "NODE", + "aggregatorPublicKey": "02d290e9edc006f4b0fbce2f77f5361dac479a47718ab68f6bc33b187139e00834", + "quorumThreshold": "1" + }, + "aliceToken": "d99880830182d99881870103d998788301410158210201094a1e421e3a02c1af74946867d89fd801972e84420244e6b199fa84d1c88758203c7ab90b2172482fdcb4e85107c9e2b16ea81704ecec7d792e2736a934b4286b5820286864a8a3adc6315b46016622103f1c3fd5be0d504323cb924219a820f39da5f6f6d998798401d998778501d9987883014101582102794247f7bb8c2dea146d7e54859c3579b716aeafec47bd4a7dc9c95828558b3d5820ea83627c6dc2d763ba5f07e58efcff360674810483800f6fcffad606d68a8499582084e1e2b80b7e46247830aa98d9e26f2e46eccb244189ffcf11dc71fd7cea75d95841aa13bc22f2779cee6f53d78012a3b8170db0a183d5100008e8aef50b3dc2071865d12e90f6be06ec5c590bca7a71d8bd9d0f82be7b15cb0f5e9499a3c5cf58d60158200000000000000000000000000000000000000000000000000000000000000000d998598701d9985a8a010000f658200aebb240e2c73397bda5a00adfba2cd97257cef004907672fdd2a0850c8adb604000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f658208d7a85069732fa3378339ca2694b40642d63f7d268ab9938815e021ac077b8cba1644e4f44455841001a9a2d110e1cf2feae864979ef26744b857aee8247f1b09d6b7cd6c1c8656d1df17a240faad11b1f573fae6214d150335e0b2bbe5ec8ac22a52cbc5057fd160180", + "bobToken": "d99880830182d99881870103d998788301410158210201094a1e421e3a02c1af74946867d89fd801972e84420244e6b199fa84d1c88758203c7ab90b2172482fdcb4e85107c9e2b16ea81704ecec7d792e2736a934b4286b5820286864a8a3adc6315b46016622103f1c3fd5be0d504323cb924219a820f39da5f6f6d998798401d998778501d9987883014101582102794247f7bb8c2dea146d7e54859c3579b716aeafec47bd4a7dc9c95828558b3d5820ea83627c6dc2d763ba5f07e58efcff360674810483800f6fcffad606d68a8499582084e1e2b80b7e46247830aa98d9e26f2e46eccb244189ffcf11dc71fd7cea75d95841aa13bc22f2779cee6f53d78012a3b8170db0a183d5100008e8aef50b3dc2071865d12e90f6be06ec5c590bca7a71d8bd9d0f82be7b15cb0f5e9499a3c5cf58d60158200000000000000000000000000000000000000000000000000000000000000000d998598701d9985a8a010000f658200aebb240e2c73397bda5a00adfba2cd97257cef004907672fdd2a0850c8adb604000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f658208d7a85069732fa3378339ca2694b40642d63f7d268ab9938815e021ac077b8cba1644e4f44455841001a9a2d110e1cf2feae864979ef26744b857aee8247f1b09d6b7cd6c1c8656d1df17a240faad11b1f573fae6214d150335e0b2bbe5ec8ac22a52cbc5057fd16018182d998858401d998788301410158210394be5745a8d545e8c3fd7e59dcd39da0f8edcd29ff1f6d98e8f0136b721be2ba582094df64fc3f5168111b03ee30ff0c43e213d3320c92cfc290122ad2f78974a176f6d998798401d998778501d998788301410158210201094a1e421e3a02c1af74946867d89fd801972e84420244e6b199fa84d1c8875820787e7ace403ead5472c23caafce2655a7e3609a3825f591302e58c0031eb76175820b714555a7187a102a474abae499c21cd817b6fd62da674bbfd46786af0ef6e73584175fd30f6a3180e73a9b1699d548fb21cf6f367b256182507cff9a57b1433d27b11313869693c6ed73ce419ba7b31dfb2819d612b5f62ba3995a40c7c00df4b9501584020000000000000000000000000000000000000000000000000000000000000000aebb240e2c73397bda5a00adfba2cd97257cef004907672fdd2a0850c8adb60d998598701d9985a8a010000f65820d6c8d918237d3a7e528de669ce22974a7600656b74ee770bfe82179ace16c97e4000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f65820dc5b452f911ce2372a539f7e28eaaafb7e6eabc07c3712a1d98ea57d356a5e8aa1644e4f444558411c3ea96557c571481fa82c68c164690fca34b77b937eb68ee4a235fde58c77435a2b1637be0b76325cc550c2e61badc077c6c74bc8078a68d6987f94d5e197d200", + "carolToken": "d99880830182d99881870103d998788301410158210201094a1e421e3a02c1af74946867d89fd801972e84420244e6b199fa84d1c88758203c7ab90b2172482fdcb4e85107c9e2b16ea81704ecec7d792e2736a934b4286b5820286864a8a3adc6315b46016622103f1c3fd5be0d504323cb924219a820f39da5f6f6d998798401d998778501d9987883014101582102794247f7bb8c2dea146d7e54859c3579b716aeafec47bd4a7dc9c95828558b3d5820ea83627c6dc2d763ba5f07e58efcff360674810483800f6fcffad606d68a8499582084e1e2b80b7e46247830aa98d9e26f2e46eccb244189ffcf11dc71fd7cea75d95841aa13bc22f2779cee6f53d78012a3b8170db0a183d5100008e8aef50b3dc2071865d12e90f6be06ec5c590bca7a71d8bd9d0f82be7b15cb0f5e9499a3c5cf58d60158200000000000000000000000000000000000000000000000000000000000000000d998598701d9985a8a010000f658200aebb240e2c73397bda5a00adfba2cd97257cef004907672fdd2a0850c8adb604000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f658208d7a85069732fa3378339ca2694b40642d63f7d268ab9938815e021ac077b8cba1644e4f44455841001a9a2d110e1cf2feae864979ef26744b857aee8247f1b09d6b7cd6c1c8656d1df17a240faad11b1f573fae6214d150335e0b2bbe5ec8ac22a52cbc5057fd16018282d998858401d998788301410158210394be5745a8d545e8c3fd7e59dcd39da0f8edcd29ff1f6d98e8f0136b721be2ba582094df64fc3f5168111b03ee30ff0c43e213d3320c92cfc290122ad2f78974a176f6d998798401d998778501d998788301410158210201094a1e421e3a02c1af74946867d89fd801972e84420244e6b199fa84d1c8875820787e7ace403ead5472c23caafce2655a7e3609a3825f591302e58c0031eb76175820b714555a7187a102a474abae499c21cd817b6fd62da674bbfd46786af0ef6e73584175fd30f6a3180e73a9b1699d548fb21cf6f367b256182507cff9a57b1433d27b11313869693c6ed73ce419ba7b31dfb2819d612b5f62ba3995a40c7c00df4b9501584020000000000000000000000000000000000000000000000000000000000000000aebb240e2c73397bda5a00adfba2cd97257cef004907672fdd2a0850c8adb60d998598701d9985a8a010000f65820d6c8d918237d3a7e528de669ce22974a7600656b74ee770bfe82179ace16c97e4000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f65820dc5b452f911ce2372a539f7e28eaaafb7e6eabc07c3712a1d98ea57d356a5e8aa1644e4f444558411c3ea96557c571481fa82c68c164690fca34b77b937eb68ee4a235fde58c77435a2b1637be0b76325cc550c2e61badc077c6c74bc8078a68d6987f94d5e197d20082d998858401d998788301410158210208344650f76b0a1c79b1b735f1d949031bc5f3a510155201cb2e9529a6cd919b5820d1a661ba4a72508795367cec99d720597deb9d5bdb5ac856dcf41519bdb32500f6d998798401d998778501d998788301410158210394be5745a8d545e8c3fd7e59dcd39da0f8edcd29ff1f6d98e8f0136b721be2ba58202d65214ce5470ff9615b370325562f8f38387fc30b54f433584d5221b6c825f0582012e9847ca74d6843679b1d41f3904f7e3bd5f8c756173e25833892047e56d417584114495a5b6fbfe68a99f34dcf7bc45b913e270f467c32ff465f23973a6fad0eb007afd15fbb01414e77ff096fec93ad2c54bc30f3286e0875b7f1ab83d85619f20158408000000000000000000000000000000000000000000000000000000000000000d6c8d918237d3a7e528de669ce22974a7600656b74ee770bfe82179ace16c97ed998598701d9985a8a010000f65820a03babab0dc839cd821fdaf51aaedd502f2f9a2693c8a49b5fbee9044cee5f4e4000f600f6f658200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880103000000f65820ab26a407db11641b7edd928c5ac36394c54a49e7bb5ae0dbc930867daf156b91a1644e4f444558418a912f4d6d1f93fcf9404e3e94884bcc4458cf2c5573fdcb971c1a72ad1253b508a23f9909cf829afee63bf08d66527ad5000b00e6e89cc8120210dddfb0d36a01" +}