Skip to content
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
7 changes: 0 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
38 changes: 38 additions & 0 deletions e2e/Cargo.lock

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

101 changes: 97 additions & 4 deletions src/api/inclusion_certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)]
Expand Down Expand Up @@ -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(&region);
hash = if bit_at(key, depth) {
h.update(sibling).update(hash.data())
} else {
Expand All @@ -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(&region)
.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));
}
}
5 changes: 4 additions & 1 deletion src/payment/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ impl Asset {

/// Encode to CBOR: `[bstr(id), bstr(value)]`.
fn to_cbor(&self) -> Vec<u8> {
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)),
])
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/payment/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
7 changes: 5 additions & 2 deletions src/payment/justification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ impl SplitMintJustification {

/// Encode to CBOR (tag 39044): `[token, [proofs...]]`.
pub fn to_cbor(&self) -> Vec<u8> {
let proof_bytes: Vec<Vec<u8>> =
self.proofs.iter().map(RsmstInclusionProof::to_cbor).collect();
let proof_bytes: Vec<Vec<u8>> = 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,
Expand Down
6 changes: 3 additions & 3 deletions src/payment/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions src/payment/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading
Loading