From 0bdbf331535c313189b266bb628ee0733b4a3038 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:19:15 -0300 Subject: [PATCH 1/9] feat: Enhance WASM build process for web and node, add universal package support --- .github/workflows/release.yml | 45 ++++++++++++++++++++++++++++------- Makefile | 14 +++++++++-- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3811033..a9424e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,12 +62,17 @@ jobs: cargo check --features "crypto,subxt-native" cargo test --features crypto-zk + - name: Build WASM package (web) + if: steps.check_tag.outputs.exists == 'false' + run: | + wasm-pack build --target web --out-dir pkg/web --release --no-opt --features crypto-zk + - name: Build WASM package (nodejs) if: steps.check_tag.outputs.exists == 'false' run: | - wasm-pack build --target nodejs --out-dir pkg --release --no-opt --features crypto-zk + wasm-pack build --target nodejs --out-dir pkg/node --release --no-opt --features crypto-zk - - name: Prepare npm package metadata + - name: Prepare universal npm package if: steps.check_tag.outputs.exists == 'false' run: | node <<'NODE' @@ -79,12 +84,13 @@ jobs: if (!versionMatch) throw new Error('Version not found in Cargo.toml'); const version = versionMatch[1]; - const pkgPath = path.join('pkg', 'package.json'); + // Use package.json from one of the builds as base template + const pkgPath = path.join('pkg', 'web', 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); pkg.name = '@orbinum/protocol-core'; pkg.version = version; - pkg.description = 'Core protocol primitives and WASM bindings for interacting with Orbinum'; + pkg.description = 'Core protocol primitives and WASM bindings for interacting with Orbinum (Universal: Node + Web)'; pkg.license = 'Apache-2.0 OR GPL-3.0-or-later'; pkg.repository = { type: 'git', @@ -93,17 +99,38 @@ jobs: pkg.homepage = 'https://github.com/orbinum/protocol-core'; pkg.publishConfig = { access: 'public' }; pkg.keywords = ['orbinum', 'protocol', 'wasm', 'substrate', 'zk']; + + // Hybrid/Universal configuration + pkg.main = './node/orbinum_protocol_core.js'; + pkg.module = './web/orbinum_protocol_core.js'; + pkg.types = './web/orbinum_protocol_core.d.ts'; // Types are identical + + pkg.files = ['web', 'node', 'README.md', 'LICENSE']; + + pkg.exports = { + ".": { + "browser": "./web/orbinum_protocol_core.js", + "node": "./node/orbinum_protocol_core.js", + "default": "./web/orbinum_protocol_core.js" + } + }; - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); + // Write new package.json to root pkg dir + fs.writeFileSync(path.join('pkg', 'package.json'), JSON.stringify(pkg, null, 2) + '\n'); NODE + # Copy artifacts to root pkg structure cp README.md pkg/README.md + # LICENSE usually copied by wasm-pack, but let's be sure to have it in root pkg if needed + # (wasm-pack puts it inside web/ and node/, good to have one at top level too?) + # For publishing, npm ignores root files not in 'files' array, but 'pkg' folder is what we publish. + - name: Create release asset if: steps.check_tag.outputs.exists == 'false' run: | - tar -czf orbinum-protocol-core-${{ steps.version.outputs.version }}-wasm-nodejs.tar.gz -C pkg . - ls -lh orbinum-protocol-core-${{ steps.version.outputs.version }}-wasm-nodejs.tar.gz + tar -czf orbinum-protocol-core-${{ steps.version.outputs.version }}-universal.tar.gz -C pkg . + ls -lh orbinum-protocol-core-${{ steps.version.outputs.version }}-universal.tar.gz - name: Ensure CHANGELOG.md exists if: steps.check_tag.outputs.exists == 'false' @@ -169,14 +196,14 @@ jobs: with: tag_name: ${{ steps.version.outputs.tag }} name: Release ${{ steps.version.outputs.version }} - files: orbinum-protocol-core-${{ steps.version.outputs.version }}-wasm-nodejs.tar.gz + files: orbinum-protocol-core-${{ steps.version.outputs.version }}-universal.tar.gz generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Clean local release artifact before cargo publish if: steps.check_tag.outputs.exists == 'false' - run: rm -f orbinum-protocol-core-${{ steps.version.outputs.version }}-wasm-nodejs.tar.gz + run: rm -f orbinum-protocol-core-${{ steps.version.outputs.version }}-universal.tar.gz - name: Publish crate to crates.io if: steps.check_tag.outputs.exists == 'false' diff --git a/Makefile b/Makefile index 65303b6..ee94cd9 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,18 @@ check: clean: @echo "🧹 Cleaning..." @cargo clean - @rm -rf pkg pkg-node + @rm -rf pkg pkg-node target @echo "βœ… Clean complete" +# Build for web (with ZK crypto, no signing) +build-web: + @echo "🌐 Building for web (with crypto-zk)..." + @wasm-pack build --target web --out-dir pkg --release --features crypto-zk + @echo "βœ… Web build complete: pkg/" + @echo " βœ… Poseidon hash available" + @echo " βœ… Commitments/Nullifiers available" + @echo " ❌ Signing NOT available (use @polkadot/keyring)" + # Build WASM for web (with ZK crypto, no signing) wasm: @echo "🌐 Building WASM for web (with crypto-zk)..." @@ -46,7 +55,7 @@ wasm-node: @echo "βœ… WASM Node build complete: pkg-node/" # Build all WASM targets -wasm-all: wasm wasm-node +wasm-all: build-web wasm wasm-node # Format code fmt: @@ -68,6 +77,7 @@ help: @echo " make test - Run all tests" @echo " make check - Check code and run clippy" @echo " make clean - Clean build artifacts" + @echo " make build-web - Build for web" @echo " make wasm - Build WASM for web" @echo " make wasm-node - Build WASM for Node.js" @echo " make wasm-all - Build all WASM targets" From 0253eff0b1006b7f06222994d68615874fadfc5e Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:29:00 -0300 Subject: [PATCH 2/9] =?UTF-8?q?feat(types):=20migrate=20Address=20from=20H?= =?UTF-8?q?160=20to=20AccountId32=20(20=20=E2=86=92=2032=20bytes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `Address([u8; 20])` with `Address([u8; 32])` throughout the SDK to align with Substrate's native AccountId32 model. All account types (Sr25519, ECDSA, EVM-mapped) are now represented uniformly as 32-byte addresses. --- .../builders/compliance/approve_disclosure.rs | 6 +-- .../builders/compliance/audit_policy.rs | 4 +- src/application/builders/compliance/mod.rs | 10 ++--- .../builders/compliance/reject_disclosure.rs | 4 +- .../builders/compliance/request_disclosure.rs | 4 +- .../builders/compliance/submit_disclosure.rs | 4 +- src/application/builders/core/mod.rs | 2 +- src/application/builders/core/shield.rs | 2 +- src/application/builders/core/transfer.rs | 2 +- src/application/builders/core/unshield.rs | 10 ++--- src/application/builders/extrinsic.rs | 4 +- src/application/builders/mod.rs | 14 +++---- src/application/errors.rs | 2 +- src/application/mod.rs | 2 +- src/application/params.rs | 12 +++--- src/application/ports/transaction_encoder.rs | 10 ++--- .../validators/compliance_validator.rs | 18 ++++---- src/application/validators/mod.rs | 2 +- .../validators/transaction_validator.rs | 2 + src/domain/entities.rs | 18 ++++---- src/domain/mod.rs | 4 +- src/domain/ports.rs | 4 +- src/domain/ports/encoder.rs | 4 +- src/domain/types/mod.rs | 4 +- src/domain/types/primitives.rs | 41 +++++++++++------- src/infrastructure/codec/encoder.rs | 4 +- src/infrastructure/codec/types.rs | 18 ++++---- src/infrastructure/crypto.rs | 4 +- .../serde_adapters/collections.rs | 6 +-- .../serde_adapters/primitives.rs | 6 +-- .../adapters/transaction_encoder.rs | 10 ++--- .../serializers/core/call_data_builder.rs | 10 ++--- .../core/transaction_serializer.rs | 2 +- .../extrinsic/extrinsic_serializer.rs | 2 +- src/presentation/api/compliance.rs | 18 ++++---- src/presentation/api/core.rs | 6 +-- src/presentation/api/extrinsic.rs | 4 +- src/presentation/api/signing.rs | 12 +++--- src/presentation/crypto_api.rs | 2 + src/presentation/wasm_bindings.rs | 42 +++++++++---------- src/presentation/zk_models.rs | 2 +- 41 files changed, 178 insertions(+), 159 deletions(-) diff --git a/src/application/builders/compliance/approve_disclosure.rs b/src/application/builders/compliance/approve_disclosure.rs index b1eea2c..8300382 100644 --- a/src/application/builders/compliance/approve_disclosure.rs +++ b/src/application/builders/compliance/approve_disclosure.rs @@ -31,7 +31,7 @@ mod tests { fn test_approve_disclosure_build_unsigned() { let encoder = SubstrateTransactionEncoder::new(); let params = ApproveDisclosureParams { - auditor: Address::from_slice_unchecked(&[1u8; 20]), + auditor: Address::from_slice_unchecked(&[1u8; 32]), commitment: Commitment::from_bytes_unchecked([2u8; 32]), zk_proof: vec![10u8; 128], disclosed_data: vec![20u8; 16], @@ -53,7 +53,7 @@ mod tests { let tx_a = ApproveDisclosureBuilder::build_unsigned( &encoder, ApproveDisclosureParams { - auditor: Address::from_slice_unchecked(&[4u8; 20]), + auditor: Address::from_slice_unchecked(&[4u8; 32]), commitment: base_commitment, zk_proof: vec![1u8; 64], disclosed_data: vec![2u8; 8], @@ -64,7 +64,7 @@ mod tests { let tx_b = ApproveDisclosureBuilder::build_unsigned( &encoder, ApproveDisclosureParams { - auditor: Address::from_slice_unchecked(&[4u8; 20]), + auditor: Address::from_slice_unchecked(&[4u8; 32]), commitment: base_commitment, zk_proof: vec![9u8; 64], disclosed_data: vec![8u8; 8], diff --git a/src/application/builders/compliance/audit_policy.rs b/src/application/builders/compliance/audit_policy.rs index d17db56..9e685ef 100644 --- a/src/application/builders/compliance/audit_policy.rs +++ b/src/application/builders/compliance/audit_policy.rs @@ -32,7 +32,7 @@ mod tests { let encoder = SubstrateTransactionEncoder::new(); let params = SetAuditPolicyParams { auditors: vec![AuditorInfo { - account: Address::from_slice_unchecked(&[5u8; 20]), + account: Address::from_slice_unchecked(&[5u8; 32]), public_key: Some([6u8; 32]), authorized_from: 100, }], @@ -55,7 +55,7 @@ mod tests { fn test_set_audit_policy_option_encoding_changes() { let encoder = SubstrateTransactionEncoder::new(); let auditors = vec![AuditorInfo { - account: Address::from_slice_unchecked(&[7u8; 20]), + account: Address::from_slice_unchecked(&[7u8; 32]), public_key: None, authorized_from: 0, }]; diff --git a/src/application/builders/compliance/mod.rs b/src/application/builders/compliance/mod.rs index bc7077a..67afaa0 100644 --- a/src/application/builders/compliance/mod.rs +++ b/src/application/builders/compliance/mod.rs @@ -34,7 +34,7 @@ mod tests { &encoder, SetAuditPolicyParams { auditors: vec![AuditorInfo { - account: Address::from_slice_unchecked(&[1u8; 20]), + account: Address::from_slice_unchecked(&[1u8; 32]), public_key: None, authorized_from: 0, }], @@ -47,7 +47,7 @@ mod tests { let request = RequestDisclosureBuilder::build_unsigned( &encoder, RequestDisclosureParams { - target: Address::from_slice_unchecked(&[2u8; 20]), + target: Address::from_slice_unchecked(&[2u8; 32]), reason: b"review".to_vec(), evidence: None, }, @@ -57,7 +57,7 @@ mod tests { let approve = ApproveDisclosureBuilder::build_unsigned( &encoder, ApproveDisclosureParams { - auditor: Address::from_slice_unchecked(&[3u8; 20]), + auditor: Address::from_slice_unchecked(&[3u8; 32]), commitment: Commitment::from_bytes_unchecked([4u8; 32]), zk_proof: vec![1u8; 64], disclosed_data: vec![2u8; 8], @@ -68,7 +68,7 @@ mod tests { let reject = RejectDisclosureBuilder::build_unsigned( &encoder, RejectDisclosureParams { - auditor: Address::from_slice_unchecked(&[5u8; 20]), + auditor: Address::from_slice_unchecked(&[5u8; 32]), reason: b"not enough".to_vec(), }, 4, @@ -81,7 +81,7 @@ mod tests { proof_bytes: vec![7u8; 64], public_signals: vec![8u8; 32], partial_data: vec![9u8; 16], - auditor: Some(Address::from_slice_unchecked(&[10u8; 20])), + auditor: Some(Address::from_slice_unchecked(&[10u8; 32])), }, 5, ); diff --git a/src/application/builders/compliance/reject_disclosure.rs b/src/application/builders/compliance/reject_disclosure.rs index 2e6574b..0bf624e 100644 --- a/src/application/builders/compliance/reject_disclosure.rs +++ b/src/application/builders/compliance/reject_disclosure.rs @@ -31,7 +31,7 @@ mod tests { fn test_reject_disclosure_build_unsigned() { let encoder = SubstrateTransactionEncoder::new(); let params = RejectDisclosureParams { - auditor: Address::from_slice_unchecked(&[21u8; 20]), + auditor: Address::from_slice_unchecked(&[21u8; 32]), reason: b"Insufficient evidence".to_vec(), }; @@ -46,7 +46,7 @@ mod tests { #[test] fn test_reject_disclosure_reason_changes_encoding() { let encoder = SubstrateTransactionEncoder::new(); - let auditor = Address::from_slice_unchecked(&[22u8; 20]); + let auditor = Address::from_slice_unchecked(&[22u8; 32]); let tx_a = RejectDisclosureBuilder::build_unsigned( &encoder, diff --git a/src/application/builders/compliance/request_disclosure.rs b/src/application/builders/compliance/request_disclosure.rs index 468ea9d..34522d6 100644 --- a/src/application/builders/compliance/request_disclosure.rs +++ b/src/application/builders/compliance/request_disclosure.rs @@ -31,7 +31,7 @@ mod tests { fn test_request_disclosure_build_unsigned() { let encoder = SubstrateTransactionEncoder::new(); let params = RequestDisclosureParams { - target: Address::from_slice_unchecked(&[15u8; 20]), + target: Address::from_slice_unchecked(&[15u8; 32]), reason: b"Suspicious pattern".to_vec(), evidence: Some(vec![42u8; 12]), }; @@ -47,7 +47,7 @@ mod tests { #[test] fn test_request_disclosure_evidence_option_changes_encoding() { let encoder = SubstrateTransactionEncoder::new(); - let target = Address::from_slice_unchecked(&[16u8; 20]); + let target = Address::from_slice_unchecked(&[16u8; 32]); let reason = b"Regulatory request".to_vec(); let with_evidence = RequestDisclosureBuilder::build_unsigned( diff --git a/src/application/builders/compliance/submit_disclosure.rs b/src/application/builders/compliance/submit_disclosure.rs index b1177ef..8d7c75b 100644 --- a/src/application/builders/compliance/submit_disclosure.rs +++ b/src/application/builders/compliance/submit_disclosure.rs @@ -35,7 +35,7 @@ mod tests { proof_bytes: vec![1u8; 96], public_signals: vec![2u8; 48], partial_data: vec![3u8; 24], - auditor: Some(Address::from_slice_unchecked(&[24u8; 20])), + auditor: Some(Address::from_slice_unchecked(&[24u8; 32])), }; let tx = SubmitDisclosureBuilder::build_unsigned(&encoder, params, 61); @@ -58,7 +58,7 @@ mod tests { proof_bytes: vec![4u8; 64], public_signals: vec![5u8; 32], partial_data: vec![6u8; 16], - auditor: Some(Address::from_slice_unchecked(&[26u8; 20])), + auditor: Some(Address::from_slice_unchecked(&[26u8; 32])), }, 0, ); diff --git a/src/application/builders/core/mod.rs b/src/application/builders/core/mod.rs index 0728851..251caad 100644 --- a/src/application/builders/core/mod.rs +++ b/src/application/builders/core/mod.rs @@ -54,7 +54,7 @@ mod tests { UnshieldParams { nullifier: Nullifier::from_bytes_unchecked([11u8; 32]), amount: 50, - recipient: Address::from_slice_unchecked(&[12u8; 20]), + recipient: Address::from_slice_unchecked(&[12u8; 32]), root: Hash::from_slice(&[13u8; 32]), proof: vec![14u8; 64], }, diff --git a/src/application/builders/core/shield.rs b/src/application/builders/core/shield.rs index 94bd028..1a5afbf 100644 --- a/src/application/builders/core/shield.rs +++ b/src/application/builders/core/shield.rs @@ -137,7 +137,7 @@ mod tests { } fn address(&self) -> Address { - Address::from_slice_unchecked(&[9u8; 20]) + Address::from_slice_unchecked(&[9u8; 32]) } fn public_key(&self) -> PublicKey { diff --git a/src/application/builders/core/transfer.rs b/src/application/builders/core/transfer.rs index 9a5192a..78941be 100644 --- a/src/application/builders/core/transfer.rs +++ b/src/application/builders/core/transfer.rs @@ -200,7 +200,7 @@ mod tests { } fn address(&self) -> Address { - Address::from_slice_unchecked(&[34u8; 20]) + Address::from_slice_unchecked(&[34u8; 32]) } fn public_key(&self) -> PublicKey { diff --git a/src/application/builders/core/unshield.rs b/src/application/builders/core/unshield.rs index 7224d58..771fba6 100644 --- a/src/application/builders/core/unshield.rs +++ b/src/application/builders/core/unshield.rs @@ -67,7 +67,7 @@ mod tests { let params = UnshieldParams { nullifier: Nullifier::from_bytes_unchecked([2u8; 32]), amount: 500u128, - recipient: Address::from_slice_unchecked(&[3u8; 20]), + recipient: Address::from_slice_unchecked(&[3u8; 32]), root: Hash::from_slice(&[4u8; 32]), proof: vec![5u8; 128], }; @@ -86,7 +86,7 @@ mod tests { fn test_unshield_with_proof_sizes() { let encoder = SubstrateTransactionEncoder::new(); let nullifier = Nullifier::from_bytes_unchecked([2u8; 32]); - let recipient = Address::from_slice_unchecked(&[3u8; 20]); + let recipient = Address::from_slice_unchecked(&[3u8; 32]); let root = Hash::from_slice(&[4u8; 32]); let params_small = UnshieldParams { @@ -119,7 +119,7 @@ mod tests { let params = UnshieldParams { nullifier: Nullifier::from_bytes_unchecked([12u8; 32]), amount: 900, - recipient: Address::from_slice_unchecked(&[13u8; 20]), + recipient: Address::from_slice_unchecked(&[13u8; 32]), root: Hash::from_slice(&[14u8; 32]), proof: vec![15u8; 128], }; @@ -140,7 +140,7 @@ mod tests { } fn address(&self) -> Address { - Address::from_slice_unchecked(&[18u8; 20]) + Address::from_slice_unchecked(&[18u8; 32]) } fn public_key(&self) -> PublicKey { @@ -155,7 +155,7 @@ mod tests { let params = UnshieldParams { nullifier: Nullifier::from_bytes_unchecked([20u8; 32]), amount: 1500, - recipient: Address::from_slice_unchecked(&[21u8; 20]), + recipient: Address::from_slice_unchecked(&[21u8; 32]), root: Hash::from_slice(&[22u8; 32]), proof: vec![23u8; 128], }; diff --git a/src/application/builders/extrinsic.rs b/src/application/builders/extrinsic.rs index 0c5aae1..d6b041b 100644 --- a/src/application/builders/extrinsic.rs +++ b/src/application/builders/extrinsic.rs @@ -54,7 +54,7 @@ mod tests { let call_data = vec![1, 2, 3]; let unsigned_tx = UnsignedTransaction::new(call_data, 0); let signature = vec![4u8; 65]; // ECDSA signature must be 65 bytes - let address = Address::from_slice_unchecked(&[7u8; 20]); + let address = Address::from_slice_unchecked(&[7u8; 32]); let signed_tx = ExtrinsicBuilder::build_signed(unsigned_tx, &signature, address); @@ -66,7 +66,7 @@ mod tests { let call_data = vec![1, 2, 3]; let unsigned_tx = UnsignedTransaction::new(call_data, 0); let signature = vec![4u8; 65]; // ECDSA signature must be 65 bytes - let address = Address::from_slice_unchecked(&[7u8; 20]); + let address = Address::from_slice_unchecked(&[7u8; 32]); let signed_tx = ExtrinsicBuilder::build_signed(unsigned_tx, &signature, address); let serialized = ExtrinsicBuilder::serialize(&signed_tx); diff --git a/src/application/builders/mod.rs b/src/application/builders/mod.rs index c1573fc..e370d68 100644 --- a/src/application/builders/mod.rs +++ b/src/application/builders/mod.rs @@ -46,7 +46,7 @@ mod integration_tests { // Sign it let signature = vec![12u8; 65]; - let address = Address::from_slice_unchecked(&[13u8; 20]); + let address = Address::from_slice_unchecked(&[13u8; 32]); let signed_tx = ExtrinsicBuilder::build_signed(unsigned_tx, &signature, address); // Serialize it @@ -62,7 +62,7 @@ mod integration_tests { let params = UnshieldParams { nullifier: Nullifier::from_bytes_unchecked([2u8; 32]), amount: 500u128, - recipient: Address::from_slice_unchecked(&[3u8; 20]), + recipient: Address::from_slice_unchecked(&[3u8; 32]), root: Hash::from_slice(&[4u8; 32]), proof: vec![5u8; 128], }; @@ -72,7 +72,7 @@ mod integration_tests { // Sign it let signature = vec![14u8; 65]; - let address = Address::from_slice_unchecked(&[15u8; 20]); + let address = Address::from_slice_unchecked(&[15u8; 32]); let signed_tx = ExtrinsicBuilder::build_signed(unsigned_tx, &signature, address); // Serialize it @@ -103,7 +103,7 @@ mod integration_tests { // Sign it let signature = vec![16u8; 65]; - let address = Address::from_slice_unchecked(&[17u8; 20]); + let address = Address::from_slice_unchecked(&[15u8; 32]); let signed_tx = ExtrinsicBuilder::build_signed(unsigned_tx, &signature, address); // Serialize it @@ -124,8 +124,8 @@ mod integration_tests { let unsigned_tx = ShieldBuilder::build_unsigned(&encoder, params, vec![0u8; 32], 0); let signature = vec![12u8; 65]; - let address1 = Address::from_slice_unchecked(&[1u8; 20]); - let address2 = Address::from_slice_unchecked(&[2u8; 20]); + let address1 = Address::from_slice_unchecked(&[1u8; 32]); + let address2 = Address::from_slice_unchecked(&[2u8; 32]); let signed_tx1 = ExtrinsicBuilder::build_signed(unsigned_tx.clone(), &signature, address1); let signed_tx2 = ExtrinsicBuilder::build_signed(unsigned_tx, &signature, address2); @@ -145,7 +145,7 @@ mod integration_tests { }; let unsigned_tx = ShieldBuilder::build_unsigned(&encoder, params, vec![0u8; 32], 0); let signature = vec![12u8; 65]; - let address = Address::from_slice_unchecked(&[13u8; 20]); + let address = Address::from_slice_unchecked(&[13u8; 32]); let signed_tx = ExtrinsicBuilder::build_signed(unsigned_tx, &signature, address); diff --git a/src/application/errors.rs b/src/application/errors.rs index 9c9c8f3..897c3f4 100644 --- a/src/application/errors.rs +++ b/src/application/errors.rs @@ -3,7 +3,7 @@ //! Unified error types for transaction building and signing operations. extern crate alloc; -use alloc::string::String; +use alloc::string::{String, ToString}; use core::fmt; // Builder Errors diff --git a/src/application/mod.rs b/src/application/mod.rs index 7f700fe..b8b1091 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -46,7 +46,7 @@ mod tests { assert!(TransactionValidator::validate_commitment(&commitment).is_ok()); let auditors = vec![crate::application::params::AuditorInfo { - account: Address::from_slice_unchecked(&[1u8; 20]), + account: Address::from_slice_unchecked(&[1u8; 32]), public_key: None, authorized_from: 1, }]; diff --git a/src/application/params.rs b/src/application/params.rs index 6f2422a..74157ef 100644 --- a/src/application/params.rs +++ b/src/application/params.rs @@ -160,7 +160,7 @@ mod tests { let unshield = UnshieldParams { nullifier: Nullifier::from_bytes_unchecked([2u8; 32]), amount: 50, - recipient: Address::from_slice_unchecked(&[3u8; 20]), + recipient: Address::from_slice_unchecked(&[3u8; 32]), root: Hash::from_slice(&[4u8; 32]), proof: vec![5u8; 64], }; @@ -217,7 +217,7 @@ mod tests { fn test_compliance_params_construction() { let params = SetAuditPolicyParams { auditors: vec![AuditorInfo { - account: Address::from_slice_unchecked(&[18u8; 20]), + account: Address::from_slice_unchecked(&[18u8; 32]), public_key: Some([19u8; 32]), authorized_from: 100, }], @@ -255,14 +255,14 @@ mod tests { #[test] fn test_disclosure_transaction_params_construction() { let request = RequestDisclosureParams { - target: Address::from_slice_unchecked(&[20u8; 20]), + target: Address::from_slice_unchecked(&[20u8; 32]), reason: b"regulatory request".to_vec(), evidence: Some(vec![21u8; 4]), }; assert!(request.evidence.is_some()); let approve = ApproveDisclosureParams { - auditor: Address::from_slice_unchecked(&[22u8; 20]), + auditor: Address::from_slice_unchecked(&[22u8; 32]), commitment: Commitment::from_bytes_unchecked([23u8; 32]), zk_proof: vec![24u8; 64], disclosed_data: vec![25u8; 12], @@ -270,7 +270,7 @@ mod tests { assert_eq!(approve.zk_proof.len(), 64); let reject = RejectDisclosureParams { - auditor: Address::from_slice_unchecked(&[26u8; 20]), + auditor: Address::from_slice_unchecked(&[26u8; 32]), reason: b"insufficient basis".to_vec(), }; assert!(!reject.reason.is_empty()); @@ -280,7 +280,7 @@ mod tests { proof_bytes: vec![28u8; 64], public_signals: vec![29u8; 16], partial_data: vec![30u8; 8], - auditor: Some(Address::from_slice_unchecked(&[31u8; 20])), + auditor: Some(Address::from_slice_unchecked(&[31u8; 32])), }; assert!(submit.auditor.is_some()); diff --git a/src/application/ports/transaction_encoder.rs b/src/application/ports/transaction_encoder.rs index f928a7d..1ad3e1a 100644 --- a/src/application/ports/transaction_encoder.rs +++ b/src/application/ports/transaction_encoder.rs @@ -147,7 +147,7 @@ mod tests { let unshield = UnshieldParams { nullifier: Nullifier::from_bytes_unchecked([4u8; 32]), amount: 20, - recipient: Address::from_slice_unchecked(&[5u8; 20]), + recipient: Address::from_slice_unchecked(&[5u8; 32]), root: Hash::from_slice(&[6u8; 32]), proof: vec![7u8; 16], }; @@ -173,7 +173,7 @@ mod tests { let set_audit_policy = SetAuditPolicyParams { auditors: vec![AuditorInfo { - account: Address::from_slice_unchecked(&[16u8; 20]), + account: Address::from_slice_unchecked(&[16u8; 32]), public_key: Some([17u8; 32]), authorized_from: 1, }], @@ -186,7 +186,7 @@ mod tests { ); let request_disclosure = RequestDisclosureParams { - target: Address::from_slice_unchecked(&[18u8; 20]), + target: Address::from_slice_unchecked(&[18u8; 32]), reason: b"reason".to_vec(), evidence: Some(vec![19u8; 4]), }; @@ -196,7 +196,7 @@ mod tests { ); let approve_disclosure = ApproveDisclosureParams { - auditor: Address::from_slice_unchecked(&[20u8; 20]), + auditor: Address::from_slice_unchecked(&[20u8; 32]), commitment: Commitment::from_bytes_unchecked([21u8; 32]), zk_proof: vec![22u8; 32], disclosed_data: vec![23u8; 8], @@ -207,7 +207,7 @@ mod tests { ); let reject_disclosure = RejectDisclosureParams { - auditor: Address::from_slice_unchecked(&[24u8; 20]), + auditor: Address::from_slice_unchecked(&[24u8; 32]), reason: b"reject".to_vec(), }; assert_eq!( diff --git a/src/application/validators/compliance_validator.rs b/src/application/validators/compliance_validator.rs index b06de3f..50b0227 100644 --- a/src/application/validators/compliance_validator.rs +++ b/src/application/validators/compliance_validator.rs @@ -4,6 +4,8 @@ use crate::application::errors::ValidationError; use crate::application::params::{AuditorInfo, DisclosureConditionType}; use crate::domain::types::Address; +use alloc::format; +use alloc::string::ToString; /// Validator for compliance-related parameters pub struct ComplianceValidator; @@ -55,7 +57,7 @@ impl ComplianceValidator { /// Validates a single auditor. pub fn validate_auditor(auditor: &AuditorInfo) -> Result<(), ValidationError> { // Validate address is not all zeros - if auditor.account.as_bytes() == &[0u8; 20] { + if auditor.account.as_bytes() == &[0u8; 32] { return Err(ValidationError::AddressInvalid { field: "auditor.account".to_string(), reason: "address cannot be all zeros".to_string(), @@ -262,7 +264,7 @@ impl ComplianceValidator { /// # Returns /// `Ok(())` if valid, `ValidationError` otherwise pub fn validate_target_address(target: &Address) -> Result<(), ValidationError> { - if target.as_bytes() == &[0u8; 20] { + if target.as_bytes() == &[0u8; 32] { return Err(ValidationError::AddressInvalid { field: "target".to_string(), reason: "target address cannot be all zeros".to_string(), @@ -307,7 +309,7 @@ mod tests { fn create_valid_auditor() -> AuditorInfo { AuditorInfo { - account: Address::from_slice_unchecked(&[1u8; 20]), + account: Address::from_slice_unchecked(&[1u8; 32]), public_key: Some([2u8; 32]), authorized_from: 100, } @@ -322,7 +324,7 @@ mod tests { #[test] fn test_validate_auditor_zero_address() { let mut auditor = create_valid_auditor(); - auditor.account = Address::from_slice_unchecked(&[0u8; 20]); + auditor.account = Address::from_slice_unchecked(&[0u8; 32]); let result = ComplianceValidator::validate_auditor(&auditor); assert!(result.is_err()); } @@ -332,7 +334,7 @@ mod tests { let auditors = vec![ create_valid_auditor(), AuditorInfo { - account: Address::from_slice_unchecked(&[2u8; 20]), + account: Address::from_slice_unchecked(&[2u8; 32]), public_key: None, authorized_from: 200, }, @@ -443,7 +445,7 @@ mod tests { fn test_validate_auditor_list_too_many() { let mut auditors = Vec::new(); for i in 0..101u8 { - let mut bytes = [0u8; 20]; + let mut bytes = [0u8; 32]; bytes[0] = i.saturating_add(1); auditors.push(AuditorInfo { account: Address::from_slice_unchecked(&bytes), @@ -557,13 +559,13 @@ mod tests { #[test] fn test_validate_target_address_success() { - let target = Address::from_slice_unchecked(&[9u8; 20]); + let target = Address::from_slice_unchecked(&[9u8; 32]); assert!(ComplianceValidator::validate_target_address(&target).is_ok()); } #[test] fn test_validate_target_address_zero() { - let target = Address::from_slice_unchecked(&[0u8; 20]); + let target = Address::from_slice_unchecked(&[0u8; 32]); let result = ComplianceValidator::validate_target_address(&target); assert!(result.is_err()); assert!(matches!( diff --git a/src/application/validators/mod.rs b/src/application/validators/mod.rs index e7917e6..422d76d 100644 --- a/src/application/validators/mod.rs +++ b/src/application/validators/mod.rs @@ -19,7 +19,7 @@ mod tests { assert!(TransactionValidator::validate_commitment(&commitment).is_ok()); let auditors = vec![AuditorInfo { - account: Address::from_slice_unchecked(&[1u8; 20]), + account: Address::from_slice_unchecked(&[1u8; 32]), public_key: None, authorized_from: 1, }]; diff --git a/src/application/validators/transaction_validator.rs b/src/application/validators/transaction_validator.rs index 20fe9d6..9dc5874 100644 --- a/src/application/validators/transaction_validator.rs +++ b/src/application/validators/transaction_validator.rs @@ -3,6 +3,8 @@ use crate::application::errors::ValidationError; use crate::domain::types::{Commitment, Nullifier}; +use alloc::format; +use alloc::string::ToString; /// Validator for transaction parameters pub struct TransactionValidator; diff --git a/src/domain/entities.rs b/src/domain/entities.rs index ebf5958..c804bbb 100644 --- a/src/domain/entities.rs +++ b/src/domain/entities.rs @@ -83,7 +83,11 @@ impl SignedTransaction { // Business rules enforcement assert!(!call_data.is_empty(), "Call data cannot be empty"); assert!(!signature.is_empty(), "Signature cannot be empty"); - assert_eq!(signature.len(), 65, "ECDSA signature must be 65 bytes"); + assert!( + signature.len() == 64 || signature.len() == 65, + "Signature must be 64 bytes (Sr25519/Ed25519) or 65 bytes (ECDSA), got {}", + signature.len() + ); SignedTransaction { call_data, @@ -160,13 +164,13 @@ mod tests { #[test] fn test_signed_transaction_new_and_accessors() { - let address = Address::from_slice_unchecked(&[7u8; 20]); + let address = Address::from_slice_unchecked(&[7u8; 32]); let signature = vec![5u8; 65]; let tx = SignedTransaction::new(vec![1u8, 2u8], signature.clone(), address, 3); assert_eq!(tx.call_data(), &[1u8, 2u8]); assert_eq!(tx.signature(), &signature); - assert_eq!(tx.address().as_bytes(), &[7u8; 20]); + assert_eq!(tx.address().as_bytes(), &[7u8; 32]); assert_eq!(tx.nonce(), 3); assert!(tx.validate().is_ok()); assert!(tx.is_ready_for_broadcast()); @@ -175,14 +179,14 @@ mod tests { #[test] #[should_panic(expected = "Signature cannot be empty")] fn test_signed_transaction_new_panics_on_empty_signature() { - let address = Address::from_slice_unchecked(&[1u8; 20]); + let address = Address::from_slice_unchecked(&[1u8; 32]); let _ = SignedTransaction::new(vec![1u8], vec![], address, 0); } #[test] - #[should_panic(expected = "ECDSA signature must be 65 bytes")] + #[should_panic(expected = "Signature must be 64 bytes")] fn test_signed_transaction_new_panics_on_invalid_signature_length() { - let address = Address::from_slice_unchecked(&[1u8; 20]); - let _ = SignedTransaction::new(vec![1u8], vec![2u8; 64], address, 0); + let address = Address::from_slice_unchecked(&[1u8; 32]); + let _ = SignedTransaction::new(vec![1u8], vec![2u8; 63], address, 0); } } diff --git a/src/domain/mod.rs b/src/domain/mod.rs index a3f6b87..d8638ad 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -14,10 +14,10 @@ mod tests { #[test] fn test_domain_reexports_types_and_entities() { - let address = Address::from_slice_unchecked(&[1u8; 20]); + let address = Address::from_slice_unchecked(&[1u8; 32]); let unsigned = UnsignedTransaction::new(vec![1u8], 0); - assert_eq!(address.as_bytes(), &[1u8; 20]); + assert_eq!(address.as_bytes(), &[1u8; 32]); assert_eq!(unsigned.nonce(), 0); } diff --git a/src/domain/ports.rs b/src/domain/ports.rs index 2f9e150..3471d9d 100644 --- a/src/domain/ports.rs +++ b/src/domain/ports.rs @@ -74,7 +74,7 @@ mod tests { } fn address(&self) -> Address { - Address::from_slice_unchecked(&[3u8; 20]) + Address::from_slice_unchecked(&[3u8; 32]) } fn public_key(&self) -> PublicKey { @@ -96,7 +96,7 @@ mod tests { let signature = signer.sign(&[9u8; 4]).unwrap(); assert_eq!(signature.to_bytes().len(), 65); - assert_eq!(signer.address().as_bytes(), &[3u8; 20]); + assert_eq!(signer.address().as_bytes(), &[3u8; 32]); assert_eq!(signer.public_key().as_bytes(), &[4u8; 64]); } diff --git a/src/domain/ports/encoder.rs b/src/domain/ports/encoder.rs index f2d7250..a8ae6e0 100644 --- a/src/domain/ports/encoder.rs +++ b/src/domain/ports/encoder.rs @@ -120,12 +120,12 @@ mod tests { assert_eq!(encoder.encode_u128(11).len(), 16); assert_eq!(encoder.encode_bytes(&[1u8, 2u8]), vec![1u8, 2u8]); - let addr = Address::from_slice_unchecked(&[1u8; 20]); + let addr = Address::from_slice_unchecked(&[1u8; 32]); let hash = Hash::from_slice(&[2u8; 32]); let commitment = Commitment::from_bytes_unchecked([3u8; 32]); let nullifier = Nullifier::from_bytes_unchecked([4u8; 32]); - assert_eq!(encoder.encode_address(&addr).len(), 20); + assert_eq!(encoder.encode_address(&addr).len(), 32); assert_eq!(encoder.encode_hash(&hash).len(), 32); assert_eq!(encoder.encode_commitment(&commitment).len(), 32); assert_eq!(encoder.encode_nullifier(&nullifier).len(), 32); diff --git a/src/domain/types/mod.rs b/src/domain/types/mod.rs index 1179406..c6c6394 100644 --- a/src/domain/types/mod.rs +++ b/src/domain/types/mod.rs @@ -20,7 +20,7 @@ mod tests { #[test] fn test_types_reexports_basic_usage() { - let address = Address::from_slice_unchecked(&[1u8; 20]); + let address = Address::from_slice_unchecked(&[1u8; 32]); let hash = Hash::from_slice(&[2u8; 32]); let commitment = Commitment::from_bytes_unchecked([3u8; 32]); let nullifier = Nullifier::from_bytes_unchecked([4u8; 32]); @@ -29,7 +29,7 @@ mod tests { let public = PublicKey::from_bytes([8u8; 64]); let asset = AssetId::new(9); - assert_eq!(address.as_bytes(), &[1u8; 20]); + assert_eq!(address.as_bytes(), &[1u8; 32]); assert_eq!(hash.as_bytes(), &[2u8; 32]); assert_eq!(commitment.as_bytes(), &[3u8; 32]); assert_eq!(nullifier.as_bytes(), &[4u8; 32]); diff --git a/src/domain/types/primitives.rs b/src/domain/types/primitives.rs index c0f361c..3b271ce 100644 --- a/src/domain/types/primitives.rs +++ b/src/domain/types/primitives.rs @@ -6,20 +6,23 @@ extern crate alloc; use crate::domain::ports::Serializable; use alloc::vec::Vec; -/// Ethereum address (H160) +/// Substrate AccountId32 address (32 bytes) +/// +/// Supports both Sr25519 accounts (public key = AccountId32 directly) and +/// ECDSA accounts (blake2_256(compressed_pubkey_33_bytes) = AccountId32). #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Address(pub [u8; 20]); +pub struct Address(pub [u8; 32]); impl Address { /// Creates an Address from a slice with validation /// /// # Errors - /// Returns error if slice is not exactly 20 bytes + /// Returns error if slice is not exactly 32 bytes pub fn from_slice(slice: &[u8]) -> Result { - if slice.len() != 20 { - return Err("Address must be exactly 20 bytes"); + if slice.len() != 32 { + return Err("Address must be exactly 32 bytes"); } - let mut bytes = [0u8; 20]; + let mut bytes = [0u8; 32]; bytes.copy_from_slice(slice); Ok(Address(bytes)) } @@ -27,14 +30,14 @@ impl Address { /// Creates an Address without validation /// /// # Safety - /// Panics if slice has less than 20 bytes. Use only with trusted input. + /// Panics if slice has less than 32 bytes. Use only with trusted input. pub fn from_slice_unchecked(slice: &[u8]) -> Self { - let mut bytes = [0u8; 20]; - bytes.copy_from_slice(&slice[..20]); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&slice[..32]); Address(bytes) } - pub fn as_bytes(&self) -> &[u8; 20] { + pub fn as_bytes(&self) -> &[u8; 32] { &self.0 } } @@ -49,6 +52,12 @@ impl Serializable for Address { } } +impl From<[u8; 32]> for Address { + fn from(bytes: [u8; 32]) -> Self { + Address(bytes) + } +} + /// Hash (H256) #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Hash(pub [u8; 32]); @@ -84,10 +93,10 @@ mod tests { #[test] fn test_address_serializable() { - let original = Address::from_slice_unchecked(&[1u8; 20]); + let original = Address::from_slice_unchecked(&[1u8; 32]); let bytes = original.to_bytes(); - assert_eq!(bytes.len(), 20); + assert_eq!(bytes.len(), 32); let deserialized = Address::from_bytes(&bytes).unwrap(); assert_eq!(original, deserialized); @@ -99,17 +108,17 @@ mod tests { let result = Address::from_bytes(&short_bytes); assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "Address must be exactly 20 bytes"); + assert_eq!(result.unwrap_err(), "Address must be exactly 32 bytes"); } #[test] fn test_address_from_slice_validation() { - let valid = Address::from_slice(&[42u8; 20]); + let valid = Address::from_slice(&[42u8; 32]); assert!(valid.is_ok()); let invalid = Address::from_slice(&[1u8; 15]); assert!(invalid.is_err()); - assert_eq!(invalid.unwrap_err(), "Address must be exactly 20 bytes"); + assert_eq!(invalid.unwrap_err(), "Address must be exactly 32 bytes"); } #[test] @@ -135,7 +144,7 @@ mod tests { #[test] fn test_serializable_roundtrip_primitives() { // Test that primitive types can be serialized and deserialized correctly - let address = Address::from_slice_unchecked(&[5u8; 20]); + let address = Address::from_slice_unchecked(&[5u8; 32]); assert_eq!( address,
::from_bytes(&address.to_bytes()).unwrap() diff --git a/src/infrastructure/codec/encoder.rs b/src/infrastructure/codec/encoder.rs index 5af5394..97f491c 100644 --- a/src/infrastructure/codec/encoder.rs +++ b/src/infrastructure/codec/encoder.rs @@ -91,12 +91,12 @@ mod tests { #[test] fn test_scale_encoder_domain_types() { let encoder = ScaleEncoder::new(); - let addr = Address::from_slice_unchecked(&[1u8; 20]); + let addr = Address::from_slice_unchecked(&[1u8; 32]); let hash = Hash::from_slice(&[2u8; 32]); let commitment = Commitment::from_bytes_unchecked([3u8; 32]); let nullifier = Nullifier::from_bytes_unchecked([4u8; 32]); - assert_eq!(encoder.encode_address(&addr).len(), 20); + assert_eq!(encoder.encode_address(&addr).len(), 32); assert_eq!(encoder.encode_hash(&hash).len(), 32); assert_eq!(encoder.encode_commitment(&commitment).len(), 32); assert_eq!(encoder.encode_nullifier(&nullifier).len(), 32); diff --git a/src/infrastructure/codec/types.rs b/src/infrastructure/codec/types.rs index e51b799..08611c2 100644 --- a/src/infrastructure/codec/types.rs +++ b/src/infrastructure/codec/types.rs @@ -8,7 +8,7 @@ use codec::{Decode, Encode}; /// Address codec wrapper. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct AddressCodec(pub [u8; 20]); +pub struct AddressCodec(pub [u8; 32]); impl From
for AddressCodec { fn from(addr: Address) -> Self { @@ -207,7 +207,7 @@ mod tests { #[test] fn test_basic_codec_wrappers_roundtrip() { - let address = Address::from_slice_unchecked(&[1u8; 20]); + let address = Address::from_slice_unchecked(&[1u8; 32]); let hash = Hash::from_slice(&[2u8; 32]); let commitment = Commitment::from_bytes_unchecked([3u8; 32]); let nullifier = Nullifier::from_bytes_unchecked([4u8; 32]); @@ -217,7 +217,7 @@ mod tests { let commitment_back: Commitment = CommitmentCodec::from(commitment).into(); let nullifier_back: Nullifier = NullifierCodec::from(nullifier).into(); - assert_eq!(address_back.as_bytes(), &[1u8; 20]); + assert_eq!(address_back.as_bytes(), &[1u8; 32]); assert_eq!(hash_back.as_bytes(), &[2u8; 32]); assert_eq!(commitment_back.as_bytes(), &[3u8; 32]); assert_eq!(nullifier_back.as_bytes(), &[4u8; 32]); @@ -236,7 +236,7 @@ mod tests { let signed = SignedTransaction::new( vec![12u8, 13u8], vec![14u8; 65], - Address::from_slice_unchecked(&[15u8; 20]), + Address::from_slice_unchecked(&[15u8; 32]), 8, ); let signed_codec = SignedTransactionCodec::from(signed); @@ -244,7 +244,7 @@ mod tests { assert_eq!(signed_back.call_data(), &[12u8, 13u8]); assert_eq!(signed_back.signature().len(), 65); - assert_eq!(signed_back.address().as_bytes(), &[15u8; 20]); + assert_eq!(signed_back.address().as_bytes(), &[15u8; 32]); assert_eq!(signed_back.nonce(), 8); } @@ -261,13 +261,13 @@ mod tests { assert_eq!(shield_codec.amount, 100); let auditor = AuditorInfo { - account: Address::from_slice_unchecked(&[18u8; 20]), + account: Address::from_slice_unchecked(&[18u8; 32]), public_key: Some([19u8; 32]), authorized_from: 21, }; let auditor_codec = AuditorInfoCodec::from(auditor); assert_eq!(auditor_codec.authorized_from, 21); - assert_eq!(auditor_codec.account.0, [18u8; 20]); + assert_eq!(auditor_codec.account.0, [18u8; 32]); let cond_amount = DisclosureConditionTypeCodec::from(DisclosureConditionType::AmountAbove(500)); @@ -303,10 +303,10 @@ mod tests { #[test] fn test_scale_encode_decode_for_codec_wrappers() { - let address_codec = AddressCodec([24u8; 20]); + let address_codec = AddressCodec([24u8; 32]); let encoded = address_codec.encode(); let decoded = AddressCodec::decode(&mut &encoded[..]).unwrap(); - assert_eq!(decoded.0, [24u8; 20]); + assert_eq!(decoded.0, [24u8; 32]); let condition_codec = DisclosureConditionTypeCodec::ManualApproval; let encoded_cond = condition_codec.encode(); diff --git a/src/infrastructure/crypto.rs b/src/infrastructure/crypto.rs index 0947d50..fc45016 100644 --- a/src/infrastructure/crypto.rs +++ b/src/infrastructure/crypto.rs @@ -247,8 +247,8 @@ mod tests { let signer = signer.unwrap(); let addr = signer.address(); - // Verificar que la direcciΓ³n tiene 20 bytes - assert_eq!(addr.as_bytes().len(), 20); + // Check that the address is 32 bytes (AccountId32) + assert_eq!(addr.as_bytes().len(), 32); } #[test] diff --git a/src/infrastructure/serde_adapters/collections.rs b/src/infrastructure/serde_adapters/collections.rs index 4624763..5b0be71 100644 --- a/src/infrastructure/serde_adapters/collections.rs +++ b/src/infrastructure/serde_adapters/collections.rs @@ -66,7 +66,7 @@ pub mod option_address { where D: Deserializer<'de>, { - let bytes: Option<[u8; 20]> = Deserialize::deserialize(deserializer)?; + let bytes: Option<[u8; 32]> = Deserialize::deserialize(deserializer)?; Ok(bytes.map(|b| Address::from_slice_unchecked(&b))) } } @@ -129,13 +129,13 @@ mod tests { #[test] fn test_option_address_adapter_some_roundtrip_json() { let original = OptionAddressWrapper { - address: Some(Address::from_slice_unchecked(&[9u8; 20])), + address: Some(Address::from_slice_unchecked(&[9u8; 32])), }; let json = serde_json::to_string(&original).unwrap(); let decoded: OptionAddressWrapper = serde_json::from_str(&json).unwrap(); - assert_eq!(decoded.address.unwrap().as_bytes(), &[9u8; 20]); + assert_eq!(decoded.address.unwrap().as_bytes(), &[9u8; 32]); } #[test] diff --git a/src/infrastructure/serde_adapters/primitives.rs b/src/infrastructure/serde_adapters/primitives.rs index 3325e15..5b076ad 100644 --- a/src/infrastructure/serde_adapters/primitives.rs +++ b/src/infrastructure/serde_adapters/primitives.rs @@ -17,7 +17,7 @@ pub mod address { where D: Deserializer<'de>, { - let bytes: [u8; 20] = Deserialize::deserialize(deserializer)?; + let bytes: [u8; 32] = Deserialize::deserialize(deserializer)?; Ok(Address::from_slice_unchecked(&bytes)) } } @@ -61,13 +61,13 @@ mod tests { #[test] fn test_address_adapter_roundtrip_json() { let original = AddressWrapper { - address: Address::from_slice_unchecked(&[1u8; 20]), + address: Address::from_slice_unchecked(&[1u8; 32]), }; let json = serde_json::to_string(&original).unwrap(); let decoded: AddressWrapper = serde_json::from_str(&json).unwrap(); - assert_eq!(decoded.address.as_bytes(), &[1u8; 20]); + assert_eq!(decoded.address.as_bytes(), &[1u8; 32]); } #[test] diff --git a/src/infrastructure/serializers/adapters/transaction_encoder.rs b/src/infrastructure/serializers/adapters/transaction_encoder.rs index e9e55c1..0048a77 100644 --- a/src/infrastructure/serializers/adapters/transaction_encoder.rs +++ b/src/infrastructure/serializers/adapters/transaction_encoder.rs @@ -150,7 +150,7 @@ mod tests { let unshield = UnshieldParams { nullifier: crate::domain::types::Nullifier::from_bytes_unchecked([3u8; 32]), amount: 10, - recipient: crate::domain::types::Address::from_slice_unchecked(&[4u8; 20]), + recipient: crate::domain::types::Address::from_slice_unchecked(&[4u8; 32]), root: crate::domain::types::Hash::from_slice(&[5u8; 32]), proof: vec![6u8; 64], }; @@ -177,7 +177,7 @@ mod tests { let set_policy = SetAuditPolicyParams { auditors: vec![AuditorInfo { - account: crate::domain::types::Address::from_slice_unchecked(&[15u8; 20]), + account: crate::domain::types::Address::from_slice_unchecked(&[15u8; 32]), public_key: None, authorized_from: 1, }], @@ -189,7 +189,7 @@ mod tests { assert_eq!(set_policy_data[1], compliance_calls::SET_AUDIT_POLICY); let request = RequestDisclosureParams { - target: crate::domain::types::Address::from_slice_unchecked(&[16u8; 20]), + target: crate::domain::types::Address::from_slice_unchecked(&[16u8; 32]), reason: b"request reason".to_vec(), evidence: Some(vec![17u8; 4]), }; @@ -198,7 +198,7 @@ mod tests { assert_eq!(request_data[1], compliance_calls::REQUEST_DISCLOSURE); let approve = ApproveDisclosureParams { - auditor: crate::domain::types::Address::from_slice_unchecked(&[18u8; 20]), + auditor: crate::domain::types::Address::from_slice_unchecked(&[18u8; 32]), commitment: crate::domain::types::Commitment::from_bytes_unchecked([19u8; 32]), zk_proof: vec![20u8; 8], disclosed_data: vec![21u8; 8], @@ -208,7 +208,7 @@ mod tests { assert_eq!(approve_data[1], compliance_calls::APPROVE_DISCLOSURE); let reject = RejectDisclosureParams { - auditor: crate::domain::types::Address::from_slice_unchecked(&[22u8; 20]), + auditor: crate::domain::types::Address::from_slice_unchecked(&[22u8; 32]), reason: b"reject reason".to_vec(), }; let reject_data = encoder.encode_reject_disclosure_call_data(&reject); diff --git a/src/infrastructure/serializers/core/call_data_builder.rs b/src/infrastructure/serializers/core/call_data_builder.rs index 70ce373..554afcd 100644 --- a/src/infrastructure/serializers/core/call_data_builder.rs +++ b/src/infrastructure/serializers/core/call_data_builder.rs @@ -272,7 +272,7 @@ mod tests { let unshield = b.build_unshield_call_data( &Nullifier::from_bytes_unchecked([3u8; 32]), 20, - &Address::from_slice_unchecked(&[4u8; 20]), + &Address::from_slice_unchecked(&[4u8; 32]), &Hash::from_slice(&[5u8; 32]), &[6u8; 64], 1, @@ -320,7 +320,7 @@ mod tests { let b = builder(); let auditors = vec![AuditorInfo { - account: Address::from_slice_unchecked(&[16u8; 20]), + account: Address::from_slice_unchecked(&[16u8; 32]), public_key: None, authorized_from: 1, }]; @@ -331,7 +331,7 @@ mod tests { assert_eq!(set_policy[1], compliance_calls::SET_AUDIT_POLICY); let request = b.build_request_disclosure_call_data( - &Address::from_slice_unchecked(&[17u8; 20]), + &Address::from_slice_unchecked(&[17u8; 32]), b"reason-test", Some(&vec![18u8; 4]), ); @@ -339,7 +339,7 @@ mod tests { assert_eq!(request[1], compliance_calls::REQUEST_DISCLOSURE); let approve = b.build_approve_disclosure_call_data( - &Address::from_slice_unchecked(&[19u8; 20]), + &Address::from_slice_unchecked(&[19u8; 32]), &Commitment::from_bytes_unchecked([20u8; 32]), &[21u8; 8], &[22u8; 8], @@ -348,7 +348,7 @@ mod tests { assert_eq!(approve[1], compliance_calls::APPROVE_DISCLOSURE); let reject = b.build_reject_disclosure_call_data( - &Address::from_slice_unchecked(&[23u8; 20]), + &Address::from_slice_unchecked(&[23u8; 32]), b"reject reason", ); assert_eq!(reject[0], PALLET_INDEX); diff --git a/src/infrastructure/serializers/core/transaction_serializer.rs b/src/infrastructure/serializers/core/transaction_serializer.rs index 6b38dbd..388dec3 100644 --- a/src/infrastructure/serializers/core/transaction_serializer.rs +++ b/src/infrastructure/serializers/core/transaction_serializer.rs @@ -18,7 +18,7 @@ mod tests { let tx = SignedTransaction::new( vec![1u8, 2u8], vec![3u8; 65], - Address::from_slice_unchecked(&[4u8; 20]), + Address::from_slice_unchecked(&[4u8; 32]), 9, ); diff --git a/src/infrastructure/serializers/extrinsic/extrinsic_serializer.rs b/src/infrastructure/serializers/extrinsic/extrinsic_serializer.rs index 2fb6f51..1caf26d 100644 --- a/src/infrastructure/serializers/extrinsic/extrinsic_serializer.rs +++ b/src/infrastructure/serializers/extrinsic/extrinsic_serializer.rs @@ -22,7 +22,7 @@ mod tests { fn test_serialize_signed_transaction() { let call_data = alloc::vec![1, 2, 3]; let signature = alloc::vec![4u8; 65]; - let address = Address::from_slice_unchecked(&[7u8; 20]); + let address = Address::from_slice_unchecked(&[7u8; 32]); let signed_tx = SignedTransaction::new(call_data, signature, address, 0); let serialized = serialize_signed_transaction(&signed_tx); diff --git a/src/presentation/api/compliance.rs b/src/presentation/api/compliance.rs index d63b209..37ec80a 100644 --- a/src/presentation/api/compliance.rs +++ b/src/presentation/api/compliance.rs @@ -31,7 +31,7 @@ impl TransactionApi { /// Builds unsigned Request Disclosure transaction. pub fn build_request_disclosure_unsigned( - target: [u8; 20], + target: [u8; 32], reason: Vec, evidence: Option>, nonce: u32, @@ -48,7 +48,7 @@ impl TransactionApi { /// Builds unsigned Approve Disclosure transaction. pub fn build_approve_disclosure_unsigned( - auditor: [u8; 20], + auditor: [u8; 32], commitment: [u8; 32], zk_proof: Vec, disclosed_data: Vec, @@ -67,7 +67,7 @@ impl TransactionApi { /// Builds unsigned Reject Disclosure transaction. pub fn build_reject_disclosure_unsigned( - auditor: [u8; 20], + auditor: [u8; 32], reason: Vec, nonce: u32, ) -> Vec { @@ -86,7 +86,7 @@ impl TransactionApi { proof_bytes: Vec, public_signals: Vec, partial_data: Vec, - auditor: Option<[u8; 20]>, + auditor: Option<[u8; 32]>, nonce: u32, ) -> Vec { let params = SubmitDisclosureParams { @@ -122,7 +122,7 @@ mod tests { fn test_build_set_audit_policy_unsigned() { let call_data = TransactionApi::build_set_audit_policy_unsigned( vec![AuditorInfo { - account: Address::from_slice_unchecked(&[1u8; 20]), + account: Address::from_slice_unchecked(&[1u8; 32]), public_key: None, authorized_from: 1, }], @@ -137,7 +137,7 @@ mod tests { #[test] fn test_build_request_disclosure_unsigned() { let call_data = TransactionApi::build_request_disclosure_unsigned( - [2u8; 20], + [2u8; 32], b"valid reason".to_vec(), Some(vec![3u8; 4]), 1, @@ -149,7 +149,7 @@ mod tests { #[test] fn test_build_approve_reject_submit_batch_disclosure_unsigned() { let approve = TransactionApi::build_approve_disclosure_unsigned( - [4u8; 20], + [4u8; 32], [5u8; 32], vec![6u8; 8], vec![7u8; 8], @@ -159,7 +159,7 @@ mod tests { assert_eq!(approve[1], compliance_calls::APPROVE_DISCLOSURE); let reject = TransactionApi::build_reject_disclosure_unsigned( - [8u8; 20], + [8u8; 32], b"reject reason".to_vec(), 3, ); @@ -171,7 +171,7 @@ mod tests { vec![10u8; 8], vec![11u8; 8], vec![12u8; 8], - Some([13u8; 20]), + Some([13u8; 32]), 4, ); assert_eq!(submit[0], PALLET_INDEX); diff --git a/src/presentation/api/core.rs b/src/presentation/api/core.rs index 2aada2e..cc0d104 100644 --- a/src/presentation/api/core.rs +++ b/src/presentation/api/core.rs @@ -38,7 +38,7 @@ impl TransactionApi { nullifier: [u8; 32], amount: u128, asset_id: u32, - recipient: [u8; 20], + recipient: [u8; 32], root: [u8; 32], proof: Vec, nonce: u32, @@ -48,7 +48,7 @@ impl TransactionApi { .expect("Invalid nullifier: cannot be all zeros"), amount, recipient: Address::from_slice(&recipient) - .expect("Invalid recipient address: must be 20 bytes"), + .expect("Invalid recipient address: must be 32 bytes"), root: Hash::from_slice(&root), proof, }; @@ -110,7 +110,7 @@ mod tests { [3u8; 32], 50, 1, - [4u8; 20], + [4u8; 32], [5u8; 32], vec![6u8; 64], 1, diff --git a/src/presentation/api/extrinsic.rs b/src/presentation/api/extrinsic.rs index b05cf98..422fc3d 100644 --- a/src/presentation/api/extrinsic.rs +++ b/src/presentation/api/extrinsic.rs @@ -15,7 +15,7 @@ impl TransactionApi { pub fn build_signed_extrinsic( call_data: Vec, signature: Vec, - address: [u8; 20], + address: [u8; 32], nonce: u32, ) -> Vec { let unsigned_tx = UnsignedTransaction::new(call_data, nonce); @@ -37,7 +37,7 @@ mod tests { let out = TransactionApi::build_signed_extrinsic( vec![1u8, 2u8, 3u8], vec![4u8; 65], - [5u8; 20], + [5u8; 32], 1, ); diff --git a/src/presentation/api/signing.rs b/src/presentation/api/signing.rs index 7d259c3..b00d947 100644 --- a/src/presentation/api/signing.rs +++ b/src/presentation/api/signing.rs @@ -60,7 +60,7 @@ impl SigningApi { nullifier: [u8; 32], amount: u128, asset_id: u32, - recipient: [u8; 20], + recipient: [u8; 32], root: [u8; 32], proof: Vec, nonce: u32, @@ -116,11 +116,11 @@ impl SigningApi { TransferBuilder::build_signed(&encoder, params, nonce, &signer) } - /// Gets Ethereum address from private key. + /// Gets Substrate AccountId32 from ECDSA private key. /// /// # Returns - /// Ethereum address (20 bytes) - pub fn get_address(private_key_hex: &str) -> Result<[u8; 20], SignerError> { + /// AccountId32 (32 bytes) = blake2_256(compressed_secp256k1_pubkey) + pub fn get_address(private_key_hex: &str) -> Result<[u8; 32], SignerError> { let signer = Self::create_signer(private_key_hex)?; Ok(*signer.address().as_bytes()) } @@ -142,7 +142,7 @@ mod tests { let address = SigningApi::get_address(valid_key()); assert!(address.is_ok()); - assert_eq!(address.unwrap().len(), 20); + assert_eq!(address.unwrap().len(), 32); } #[test] @@ -161,7 +161,7 @@ mod tests { [4u8; 32], 50, 1, - [5u8; 20], + [5u8; 32], [6u8; 32], vec![7u8; 64], 1, diff --git a/src/presentation/crypto_api.rs b/src/presentation/crypto_api.rs index abc31a3..f4d986f 100644 --- a/src/presentation/crypto_api.rs +++ b/src/presentation/crypto_api.rs @@ -6,6 +6,8 @@ use crate::domain::types::identifiers::AssetId; use crate::infrastructure::crypto::ZkCryptoProvider; #[cfg(any(feature = "crypto-zk", feature = "crypto"))] use crate::presentation::zk_models::{NoteData, NullifierData}; +use alloc::format; +use alloc::string::String; #[cfg(any(feature = "crypto-zk", feature = "crypto"))] use orbinum_zk_core::NoteDto; diff --git a/src/presentation/wasm_bindings.rs b/src/presentation/wasm_bindings.rs index 8f7d79f..a47b6ee 100644 --- a/src/presentation/wasm_bindings.rs +++ b/src/presentation/wasm_bindings.rs @@ -76,15 +76,15 @@ impl TransactionBuilder { if nullifier.len() != 32 { return Err(JsValue::from_str("Nullifier must be 32 bytes")); } - if recipient.len() != 20 { - return Err(JsValue::from_str("Recipient must be 20 bytes")); + if recipient.len() != 32 { + return Err(JsValue::from_str("Recipient must be 32 bytes")); } if root.len() != 32 { return Err(JsValue::from_str("Root must be 32 bytes")); } let mut nullifier_bytes = [0u8; 32]; - let mut recipient_bytes = [0u8; 20]; + let mut recipient_bytes = [0u8; 32]; let mut root_bytes = [0u8; 32]; nullifier_bytes.copy_from_slice(&nullifier); recipient_bytes.copy_from_slice(&recipient); @@ -243,10 +243,10 @@ impl TransactionBuilder { js_sys::Reflect::get(&item, &JsValue::from_str("authorizedFrom"))?; let account = js_sys::Uint8Array::new(&account_val).to_vec(); - if account.len() != 20 { - return Err(JsValue::from_str("Auditor account must be 20 bytes")); + if account.len() != 32 { + return Err(JsValue::from_str("Auditor account must be 32 bytes")); } - let mut account_bytes = [0u8; 20]; + let mut account_bytes = [0u8; 32]; account_bytes.copy_from_slice(&account); let public_key = if public_key_val.is_null() || public_key_val.is_undefined() { @@ -320,11 +320,11 @@ impl TransactionBuilder { evidence: Option>, nonce: u32, ) -> Result, JsValue> { - if target.len() != 20 { - return Err(JsValue::from_str("Target must be 20 bytes")); + if target.len() != 32 { + return Err(JsValue::from_str("Target must be 32 bytes")); } - let mut target_bytes = [0u8; 20]; + let mut target_bytes = [0u8; 32]; target_bytes.copy_from_slice(&target); Ok(TransactionApi::build_request_disclosure_unsigned( @@ -344,14 +344,14 @@ impl TransactionBuilder { disclosed_data: Vec, nonce: u32, ) -> Result, JsValue> { - if auditor.len() != 20 { - return Err(JsValue::from_str("Auditor must be 20 bytes")); + if auditor.len() != 32 { + return Err(JsValue::from_str("Auditor must be 32 bytes")); } if commitment.len() != 32 { return Err(JsValue::from_str("Commitment must be 32 bytes")); } - let mut auditor_bytes = [0u8; 20]; + let mut auditor_bytes = [0u8; 32]; let mut commitment_bytes = [0u8; 32]; auditor_bytes.copy_from_slice(&auditor); commitment_bytes.copy_from_slice(&commitment); @@ -372,11 +372,11 @@ impl TransactionBuilder { reason: Vec, nonce: u32, ) -> Result, JsValue> { - if auditor.len() != 20 { - return Err(JsValue::from_str("Auditor must be 20 bytes")); + if auditor.len() != 32 { + return Err(JsValue::from_str("Auditor must be 32 bytes")); } - let mut auditor_bytes = [0u8; 20]; + let mut auditor_bytes = [0u8; 32]; auditor_bytes.copy_from_slice(&auditor); Ok(TransactionApi::build_reject_disclosure_unsigned( @@ -405,10 +405,10 @@ impl TransactionBuilder { let auditor_bytes = match auditor { Some(a) => { - if a.len() != 20 { - return Err(JsValue::from_str("Auditor must be 20 bytes")); + if a.len() != 32 { + return Err(JsValue::from_str("Auditor must be 32 bytes")); } - let mut out = [0u8; 20]; + let mut out = [0u8; 32]; out.copy_from_slice(&a); Some(out) } @@ -472,11 +472,11 @@ impl TransactionBuilder { address: Vec, nonce: u32, ) -> Result, JsValue> { - if address.len() != 20 { - return Err(JsValue::from_str("Address must be 20 bytes")); + if address.len() != 32 { + return Err(JsValue::from_str("Address must be 32 bytes")); } - let mut address_bytes = [0u8; 20]; + let mut address_bytes = [0u8; 32]; address_bytes.copy_from_slice(&address); Ok(TransactionApi::build_signed_extrinsic( diff --git a/src/presentation/zk_models.rs b/src/presentation/zk_models.rs index 590e057..121c09e 100644 --- a/src/presentation/zk_models.rs +++ b/src/presentation/zk_models.rs @@ -118,7 +118,7 @@ mod tests { fn test_reexported_compliance_types_are_usable() { let params = SetAuditPolicyParams { auditors: vec![AuditorInfo { - account: crate::domain::types::Address::from_slice_unchecked(&[10u8; 20]), + account: crate::domain::types::Address::from_slice_unchecked(&[10u8; 32]), public_key: Some([11u8; 32]), authorized_from: 1, }], From 11e477796f291ebafe5172627181684f012ba86b Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:30:12 -0300 Subject: [PATCH 3/9] feat: Replace HashMap with BTreeMap for wallet balances and update related methods --- src/application/note_manager.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/application/note_manager.rs b/src/application/note_manager.rs index 1860e81..5f45e61 100644 --- a/src/application/note_manager.rs +++ b/src/application/note_manager.rs @@ -2,6 +2,8 @@ //! //! Structures and utilities for managing notes and wallet state. +use alloc::collections::BTreeMap; +use alloc::vec::Vec; use orbinum_encrypted_memo::MemoData; /// Scanned note from the blockchain. @@ -69,14 +71,14 @@ impl ScannedNote { /// Wallet balance by asset. #[derive(Debug, Clone, Default)] pub struct WalletBalance { - balances: std::collections::HashMap, + balances: BTreeMap, } impl WalletBalance { /// Creates a new empty balance. pub fn new() -> Self { Self { - balances: std::collections::HashMap::new(), + balances: BTreeMap::new(), } } @@ -94,7 +96,7 @@ impl WalletBalance { } /// Gets all balances. - pub fn get_all_balances(&self) -> &std::collections::HashMap { + pub fn get_all_balances(&self) -> &BTreeMap { &self.balances } @@ -128,7 +130,7 @@ impl NoteSelector { } // Sort by value (descending) for greedy selection - suitable_notes.sort_by_key(|note| core::cmp::Reverse(note.value())); + suitable_notes.sort_by_key(|note: &ScannedNote| core::cmp::Reverse(note.value())); let mut selected = Vec::new(); let mut total = 0u64; @@ -155,7 +157,7 @@ impl NoteSelector { target_amount: u64, asset_id: u32, ) -> Result<(ScannedNote, ScannedNote, u64), &'static str> { - let (selected, change) = + let (selected, change): (Vec, u64) = Self::select_notes_for_transfer(available_notes, target_amount, asset_id)?; if selected.len() < 2 { From da1d04bd1f339a212ec037fc117c12db81855f4d Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:34:27 -0300 Subject: [PATCH 4/9] feat(disclosure): add disclosure circuit support and no_std WASM compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ZK disclosure witness generation to the SDK and fix compatibility for WASM targets (browser + Node.js) by removing std dependencies. Disclosure circuit (feat): - infrastructure/crypto: expose poseidon_hash_1(), u64_to_field_bytes(), make bytes_to_field/field_to_bytes pub(crate) - presentation/wasm_bindings: new buildDisclosureInputs() WASM export β€” builds circuit inputs for the disclosure proof (value, owner, asset_id, commitment, selective disclosure mask) - application/builders/compliance/disclosure_flow: create_disclosure_witness - Cargo.toml: orbinum-zk-core 0.4.0β†’0.5.0, orbinum-encrypted-memo 0.2.2β†’0.3.0, add blake2 = "0.10" (optional, crypto-signing gate) --- Cargo.toml | 19 +- benches/transaction_api_bench.rs | 36 +- .../builders/compliance/disclosure_flow.rs | 1093 +++++++++++++++++ src/application/builders/compliance/mod.rs | 1 + src/application/disclosure.rs | 687 +++++++++++ src/application/key_manager.rs | 12 +- src/application/memo_utils.rs | 12 +- src/application/mod.rs | 6 + src/infrastructure/crypto.rs | 49 +- src/lib.rs | 7 +- src/presentation/wasm_bindings.rs | 117 ++ 11 files changed, 2009 insertions(+), 30 deletions(-) create mode 100644 src/application/builders/compliance/disclosure_flow.rs create mode 100644 src/application/disclosure.rs diff --git a/Cargo.toml b/Cargo.toml index 126da40..82eb904 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,11 +29,18 @@ std = [ "hex/std", "orbinum-zk-core/std", "orbinum-encrypted-memo/std", + "orbinum-encrypted-memo/encrypt", ] # Crypto features split: ZK ops (WASM-compatible) vs Signing (native-only) -crypto-zk = [] # Poseidon, commitments, nullifiers - uses orbinum-zk-core (always available) -crypto-signing = ["libsecp256k1", "tiny-keccak"] # ECDSA signing for native builds -crypto = ["crypto-zk", "crypto-signing"] # Full crypto for native +crypto-zk = [ + "orbinum-encrypted-memo/encrypt", +] # Poseidon, commitments, nullifiers - uses orbinum-zk-core (always available) +crypto-signing = [ + "libsecp256k1", + "tiny-keccak", + "blake2", +] # ECDSA signing for native builds +crypto = ["crypto-zk", "crypto-signing"] # Full crypto for native subxt-native = ["subxt", "subxt/native"] subxt-web = ["subxt", "subxt/web"] @@ -51,17 +58,17 @@ hex = { version = "0.4.3", default-features = false, features = [ ] } # ZK cryptographic primitives -orbinum-zk-core = { version = "0.4.0", default-features = false } +orbinum-zk-core = { version = "0.5.0", default-features = false } # Encrypted memo primitive from Orbinum Node -orbinum-encrypted-memo = { version = "0.2.2", default-features = false, features = [ +orbinum-encrypted-memo = { version = "0.3.0", default-features = false, features = [ "parity-scale-codec", - "encrypt", ] } # Optional crypto deps libsecp256k1 = { version = "0.7.1", optional = true } tiny-keccak = { version = "2.0.2", optional = true, features = ["keccak"] } +blake2 = { version = "0.10", default-features = false, optional = true } # Subxt - not required for basic transaction building subxt = { version = "0.38.0", optional = true, default-features = false } diff --git a/benches/transaction_api_bench.rs b/benches/transaction_api_bench.rs index 9d1b992..9600cda 100644 --- a/benches/transaction_api_bench.rs +++ b/benches/transaction_api_bench.rs @@ -35,7 +35,7 @@ fn bench_build_unshield_unsigned(c: &mut Criterion) { black_box([3u8; 32]), black_box(500u128), black_box(1u32), - black_box([4u8; 20]), + black_box([4u8; 32]), black_box([5u8; 32]), black_box(vec![6u8; 192]), black_box(1u32), @@ -93,7 +93,7 @@ fn bench_serialize_signed_transaction(c: &mut Criterion) { let tx = SignedTransaction::new( vec![31u8; 128], vec![32u8; 65], - Address::from_slice_unchecked(&[33u8; 20]), + Address::from_slice_unchecked(&[33u8; 32]), 42, ); @@ -152,6 +152,37 @@ fn bench_crypto_api_poseidon_hash_2(c: &mut Criterion) { }); } +/// Measures the time to create a disclosure witness. +/// +/// This covers: 2Γ— field element conversion + poseidon_hash_1 + mask evaluation. +/// The target is < 100ms per witness. +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +fn bench_build_disclosure_witness(c: &mut Criterion) { + use orbinum_encrypted_memo::{DisclosureMask, MemoData}; + use orbinum_protocol_core::application::disclosure::create_disclosure_witness; + + let memo = MemoData::new(100_000_000u64, [0x11u8; 32], [0x99u8; 32], 0u32); + let commitment = [0x42u8; 32]; + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: true, + disclose_asset_id: true, + disclose_blinding: false, + }; + + c.bench_function("disclosure.create_witness", |b| { + b.iter(|| { + let w = create_disclosure_witness( + black_box(&memo), + black_box(&commitment), + black_box(&mask), + ) + .expect("benchmark witness should succeed"); + black_box(w) + }) + }); +} + #[cfg(feature = "crypto")] fn bench_sign_and_build_shield(c: &mut Criterion) { const PRIVATE_KEY_HEX: &str = @@ -184,6 +215,7 @@ criterion_group!( bench_crypto_api_compute_commitment, bench_crypto_api_compute_nullifier, bench_crypto_api_poseidon_hash_2, + bench_build_disclosure_witness, bench_sign_and_build_shield ); diff --git a/src/application/builders/compliance/disclosure_flow.rs b/src/application/builders/compliance/disclosure_flow.rs new file mode 100644 index 0000000..983934f --- /dev/null +++ b/src/application/builders/compliance/disclosure_flow.rs @@ -0,0 +1,1093 @@ +//! Disclosure flow integration tests. +//! +//! Validates the full client-side selective disclosure pipeline: +//! +//! 1. `create_disclosure_witness` β†’ `DisclosureWitness` (application layer) +//! 2. Packing `DisclosureWitness` fields into the 76-byte `public_signals` +//! format expected by pallet-shielded-pool's `submit_disclosure` extrinsic: +//! - bytes [0..32] = commitment +//! - bytes [32..40] = revealed_value (LE u64, 8 bytes) +//! - bytes [40..44] = revealed_asset_id (LE u32, 4 bytes) +//! - bytes [44..76] = revealed_owner_hash (32 bytes) +//! 3. Building signed-ready `UnsignedTransaction` objects via the compliance builders. +//! 4. Compliance policy lifecycle: set_policy β†’ request β†’ approve / reject. +//! 5. Batch submit with multiple entries. + +/// Extracts the 76-byte `public_signals` slice expected by pallet-shielded-pool +/// from a `DisclosureWitness`. +/// +/// The pallet's `DisclosureValidationService::verify_disclosure_proof` parses: +/// - [0..32] β†’ commitment +/// - [32..40] β†’ revealed_value (u64 LE) +/// - [40..44] β†’ revealed_asset_id (u32 LE) +/// - [44..76] β†’ revealed_owner_hash +/// +/// The witness stores value and asset_id as 32-byte field elements (BN254 scalar +/// field, little-endian canonical form), so the first 8 / 4 bytes are exactly the +/// LE u64 / u32 byte representation for values that fit in those widths. +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +pub fn disclosure_witness_to_public_signals( + witness: &crate::application::disclosure::DisclosureWitness, +) -> Vec { + let mut signals = Vec::with_capacity(76); + signals.extend_from_slice(&witness.commitment); // [0..32] + signals.extend_from_slice(&witness.revealed_value[0..8]); // [32..40] LE u64 + signals.extend_from_slice(&witness.revealed_asset_id[0..4]); // [40..44] LE u32 + signals.extend_from_slice(&witness.revealed_owner_hash); // [44..76] + signals +} + +// ─── tests ─────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use crate::application::params::{ + ApproveDisclosureParams, AuditorInfo, BatchSubmitDisclosureParams, DisclosureConditionType, + DisclosureSubmission, RejectDisclosureParams, RequestDisclosureParams, + SetAuditPolicyParams, SubmitDisclosureParams, + }; + use crate::domain::types::{Address, Commitment}; + use crate::infrastructure::serializers::SubstrateTransactionEncoder; + use crate::presentation::config::{compliance_calls, PALLET_INDEX}; + + use super::super::{ + ApproveDisclosureBuilder, BatchSubmitDisclosureBuilder, RejectDisclosureBuilder, + RequestDisclosureBuilder, SetAuditPolicyBuilder, SubmitDisclosureBuilder, + }; + + // ── helpers ────────────────────────────────────────────────────────────── + + fn encoder() -> SubstrateTransactionEncoder { + SubstrateTransactionEncoder::new() + } + + fn commitment(byte: u8) -> Commitment { + Commitment::from_bytes_unchecked([byte; 32]) + } + + fn address(byte: u8) -> Address { + Address::from_slice_unchecked(&[byte; 32]) + } + + // ── builder call_index contracts ───────────────────────────────────────── + + #[test] + fn test_all_compliance_builders_have_correct_pallet_and_call_indices() { + let enc = encoder(); + + let set_policy = SetAuditPolicyBuilder::build_unsigned( + &enc, + SetAuditPolicyParams { + auditors: vec![AuditorInfo { + account: address(1), + public_key: None, + authorized_from: 0, + }], + conditions: vec![DisclosureConditionType::ManualApproval], + max_frequency: Some(5), + }, + 0, + ); + let request = RequestDisclosureBuilder::build_unsigned( + &enc, + RequestDisclosureParams { + target: address(2), + reason: b"audit".to_vec(), + evidence: None, + }, + 1, + ); + let approve = ApproveDisclosureBuilder::build_unsigned( + &enc, + ApproveDisclosureParams { + auditor: address(3), + commitment: commitment(4), + zk_proof: vec![0u8; 64], + disclosed_data: vec![0u8; 8], + }, + 2, + ); + let reject = RejectDisclosureBuilder::build_unsigned( + &enc, + RejectDisclosureParams { + auditor: address(5), + reason: b"denied".to_vec(), + }, + 3, + ); + let submit = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: commitment(6), + proof_bytes: vec![0u8; 256], + public_signals: vec![0u8; 76], + partial_data: vec![0u8; 8], + auditor: None, + }, + 4, + ); + let batch = BatchSubmitDisclosureBuilder::build_unsigned( + &enc, + BatchSubmitDisclosureParams { + submissions: vec![DisclosureSubmission { + commitment: commitment(7), + proof: vec![0u8; 256], + public_signals: vec![0u8; 76], + disclosed_data: vec![0u8; 8], + }], + }, + 5, + ); + + for (name, tx, expected_call) in [ + ( + "set_policy", + &set_policy, + compliance_calls::SET_AUDIT_POLICY, + ), + ("request", &request, compliance_calls::REQUEST_DISCLOSURE), + ("approve", &approve, compliance_calls::APPROVE_DISCLOSURE), + ("reject", &reject, compliance_calls::REJECT_DISCLOSURE), + ("submit", &submit, compliance_calls::SUBMIT_DISCLOSURE), + ("batch", &batch, compliance_calls::BATCH_SUBMIT_DISCLOSURE), + ] { + assert_eq!( + tx.call_data()[0], + PALLET_INDEX, + "{name}: wrong pallet index" + ); + assert_eq!(tx.call_data()[1], expected_call, "{name}: wrong call index"); + assert!(tx.call_data().len() > 2, "{name}: call_data too short"); + } + } + + // ── nonce propagation ──────────────────────────────────────────────────── + + #[test] + fn test_nonce_is_propagated_into_unsigned_transaction() { + let enc = encoder(); + + for nonce in [0u32, 1, 42, u32::MAX] { + let tx = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: commitment(0), + proof_bytes: vec![0u8; 8], + public_signals: vec![0u8; 8], + partial_data: vec![], + auditor: None, + }, + nonce, + ); + assert_eq!(tx.nonce(), nonce, "nonce must be forwarded unchanged"); + } + } + + // ── public_signals packing format ───────────────────────────────────────── + + /// Constructs a 76-byte `public_signals` buffer directly from raw values. + /// This mirrors what `disclosure_witness_to_public_signals` does internally + /// and what pallet `DisclosureValidationService` decodes. + fn pack_public_signals( + commitment_bytes: &[u8; 32], + revealed_value: u64, + revealed_asset_id: u32, + revealed_owner_hash: &[u8; 32], + ) -> Vec { + let mut signals = Vec::with_capacity(76); + signals.extend_from_slice(commitment_bytes); + signals.extend_from_slice(&revealed_value.to_le_bytes()); + signals.extend_from_slice(&revealed_asset_id.to_le_bytes()); + signals.extend_from_slice(revealed_owner_hash); + signals + } + + #[test] + fn test_public_signals_length_is_76_bytes() { + let signals = pack_public_signals(&[1u8; 32], 100, 7, &[2u8; 32]); + assert_eq!(signals.len(), 76); + } + + #[test] + fn test_public_signals_commitment_occupies_first_32_bytes() { + let commit = [0xABu8; 32]; + let signals = pack_public_signals(&commit, 0, 0, &[0u8; 32]); + assert_eq!(&signals[0..32], &commit); + } + + #[test] + fn test_public_signals_value_occupies_bytes_32_to_40_as_le_u64() { + let value: u64 = 1_000_000; + let signals = pack_public_signals(&[0u8; 32], value, 0, &[0u8; 32]); + let decoded = u64::from_le_bytes(signals[32..40].try_into().unwrap()); + assert_eq!(decoded, value); + } + + #[test] + fn test_public_signals_max_u64_value_round_trips() { + let signals = pack_public_signals(&[0u8; 32], u64::MAX, 0, &[0u8; 32]); + let decoded = u64::from_le_bytes(signals[32..40].try_into().unwrap()); + assert_eq!(decoded, u64::MAX); + } + + #[test] + fn test_public_signals_asset_id_occupies_bytes_40_to_44_as_le_u32() { + let asset_id: u32 = 42; + let signals = pack_public_signals(&[0u8; 32], 0, asset_id, &[0u8; 32]); + let decoded = u32::from_le_bytes(signals[40..44].try_into().unwrap()); + assert_eq!(decoded, asset_id); + } + + #[test] + fn test_public_signals_owner_hash_occupies_last_32_bytes() { + let owner_hash = [0xCDu8; 32]; + let signals = pack_public_signals(&[0u8; 32], 0, 0, &owner_hash); + assert_eq!(&signals[44..76], &owner_hash); + } + + #[test] + fn test_public_signals_zero_value_and_asset_produce_zero_bytes() { + let signals = pack_public_signals(&[0u8; 32], 0, 0, &[0u8; 32]); + assert_eq!(&signals[32..40], &[0u8; 8]); + assert_eq!(&signals[40..44], &[0u8; 4]); + } + + /// When a field is not disclosed the pallet expects zero bytes in its slot. + #[test] + fn test_undisclosed_fields_produce_zero_slots_in_signals() { + // Not disclosing value β†’ revealed_value = 0 + let signals = pack_public_signals(&[1u8; 32], 0, 5, &[0u8; 32]); + assert_eq!( + &signals[32..40], + &[0u8; 8], + "undisclosed value must be zero" + ); + // Not disclosing owner β†’ revealed_owner_hash = [0;32] + assert_eq!( + &signals[44..76], + &[0u8; 32], + "undisclosed owner must be zero" + ); + } + + // ── submit_disclosure encoding ties to packing ──────────────────────────── + + #[test] + fn test_submit_disclosure_accepts_76_byte_public_signals() { + let enc = encoder(); + let signals = pack_public_signals(&[1u8; 32], 500, 3, &[2u8; 32]); + + let tx = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: commitment(1), + proof_bytes: vec![0u8; 256], + public_signals: signals, + partial_data: vec![], + auditor: None, + }, + 0, + ); + + assert_eq!(tx.call_data()[0], PALLET_INDEX); + assert_eq!(tx.call_data()[1], compliance_calls::SUBMIT_DISCLOSURE); + } + + #[test] + fn test_submit_disclosure_encoding_changes_with_different_signal_values() { + let enc = encoder(); + let commitment_bytes = [5u8; 32]; + let commit = Commitment::from_bytes_unchecked(commitment_bytes); + + let tx_a = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: commit, + proof_bytes: vec![0u8; 256], + public_signals: pack_public_signals(&commitment_bytes, 100, 1, &[0u8; 32]), + partial_data: vec![], + auditor: None, + }, + 0, + ); + + let tx_b = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: commit, + proof_bytes: vec![0u8; 256], + public_signals: pack_public_signals(&commitment_bytes, 200, 1, &[0u8; 32]), + partial_data: vec![], + auditor: None, + }, + 0, + ); + + assert_ne!( + tx_a.call_data(), + tx_b.call_data(), + "different revealed values must produce different call_data" + ); + } + + #[test] + fn test_submit_disclosure_with_auditor_differs_from_without() { + let enc = encoder(); + let signals = pack_public_signals(&[1u8; 32], 100, 0, &[0u8; 32]); + + let with_auditor = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: commitment(1), + proof_bytes: vec![0u8; 8], + public_signals: signals.clone(), + partial_data: vec![], + auditor: Some(address(10)), + }, + 0, + ); + + let without_auditor = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: commitment(1), + proof_bytes: vec![0u8; 8], + public_signals: signals, + partial_data: vec![], + auditor: None, + }, + 0, + ); + + assert_ne!( + with_auditor.call_data(), + without_auditor.call_data(), + "auditor presence must change the encoding" + ); + } + + // ── batch submit ───────────────────────────────────────────────────────── + + #[test] + fn test_batch_submit_single_entry_is_longer_than_minimal_header() { + let enc = encoder(); + + let tx = BatchSubmitDisclosureBuilder::build_unsigned( + &enc, + BatchSubmitDisclosureParams { + submissions: vec![DisclosureSubmission { + commitment: commitment(1), + proof: vec![0u8; 256], + public_signals: pack_public_signals(&[1u8; 32], 100, 1, &[0u8; 32]), + disclosed_data: vec![0u8; 8], + }], + }, + 0, + ); + + assert_eq!(tx.call_data()[0], PALLET_INDEX); + assert_eq!(tx.call_data()[1], compliance_calls::BATCH_SUBMIT_DISCLOSURE); + // Must encode at least pallet_byte + call_byte + length_prefix + entry + assert!(tx.call_data().len() > 10); + } + + #[test] + fn test_batch_submit_grows_with_each_submission() { + let enc = encoder(); + + let make_submission = |byte: u8| DisclosureSubmission { + commitment: commitment(byte), + proof: vec![byte; 64], + public_signals: pack_public_signals(&[byte; 32], byte as u64, byte as u32, &[byte; 32]), + disclosed_data: vec![byte; 8], + }; + + let tx_one = BatchSubmitDisclosureBuilder::build_unsigned( + &enc, + BatchSubmitDisclosureParams { + submissions: vec![make_submission(1)], + }, + 0, + ); + + let tx_two = BatchSubmitDisclosureBuilder::build_unsigned( + &enc, + BatchSubmitDisclosureParams { + submissions: vec![make_submission(1), make_submission(2)], + }, + 0, + ); + + let tx_three = BatchSubmitDisclosureBuilder::build_unsigned( + &enc, + BatchSubmitDisclosureParams { + submissions: vec![make_submission(1), make_submission(2), make_submission(3)], + }, + 0, + ); + + assert!( + tx_one.call_data().len() < tx_two.call_data().len(), + "two submissions must be longer than one" + ); + assert!( + tx_two.call_data().len() < tx_three.call_data().len(), + "three submissions must be longer than two" + ); + } + + #[test] + fn test_batch_submit_different_commitments_produce_different_call_data() { + let enc = encoder(); + + let tx_a = BatchSubmitDisclosureBuilder::build_unsigned( + &enc, + BatchSubmitDisclosureParams { + submissions: vec![DisclosureSubmission { + commitment: commitment(0xAA), + proof: vec![1u8; 64], + public_signals: pack_public_signals(&[0xAAu8; 32], 100, 1, &[0u8; 32]), + disclosed_data: vec![0u8; 8], + }], + }, + 0, + ); + + let tx_b = BatchSubmitDisclosureBuilder::build_unsigned( + &enc, + BatchSubmitDisclosureParams { + submissions: vec![DisclosureSubmission { + commitment: commitment(0xBB), + proof: vec![1u8; 64], + public_signals: pack_public_signals(&[0xBBu8; 32], 100, 1, &[0u8; 32]), + disclosed_data: vec![0u8; 8], + }], + }, + 0, + ); + + assert_ne!(tx_a.call_data(), tx_b.call_data()); + } + + // ── policy lifecycle builders ───────────────────────────────────────────── + + #[test] + fn test_set_audit_policy_with_unlimited_frequency() { + let enc = encoder(); + + let tx = SetAuditPolicyBuilder::build_unsigned( + &enc, + SetAuditPolicyParams { + auditors: vec![AuditorInfo { + account: address(1), + public_key: None, + authorized_from: 0, + }], + conditions: vec![DisclosureConditionType::ManualApproval], + max_frequency: None, // unlimited + }, + 0, + ); + + assert_eq!(tx.call_data()[1], compliance_calls::SET_AUDIT_POLICY); + assert!(tx.call_data().len() > 2); + } + + #[test] + fn test_set_audit_policy_limited_vs_unlimited_frequency_differ() { + let enc = encoder(); + + let build = |max_frequency| { + SetAuditPolicyBuilder::build_unsigned( + &enc, + SetAuditPolicyParams { + auditors: vec![AuditorInfo { + account: address(1), + public_key: None, + authorized_from: 0, + }], + conditions: vec![DisclosureConditionType::ManualApproval], + max_frequency, + }, + 0, + ) + }; + + assert_ne!( + build(Some(1)).call_data(), + build(None).call_data(), + "Some(n) vs None frequency must differ" + ); + } + + #[test] + fn test_set_audit_policy_with_multiple_auditors() { + let enc = encoder(); + + let single = SetAuditPolicyBuilder::build_unsigned( + &enc, + SetAuditPolicyParams { + auditors: vec![AuditorInfo { + account: address(1), + public_key: None, + authorized_from: 0, + }], + conditions: vec![DisclosureConditionType::ManualApproval], + max_frequency: None, + }, + 0, + ); + + let multiple = SetAuditPolicyBuilder::build_unsigned( + &enc, + SetAuditPolicyParams { + auditors: vec![ + AuditorInfo { + account: address(1), + public_key: None, + authorized_from: 0, + }, + AuditorInfo { + account: address(2), + public_key: None, + authorized_from: 0, + }, + ], + conditions: vec![DisclosureConditionType::ManualApproval], + max_frequency: None, + }, + 0, + ); + + assert!( + multiple.call_data().len() > single.call_data().len(), + "more auditors must produce longer call_data" + ); + } + + #[test] + fn test_request_disclosure_with_and_without_evidence_differ() { + let enc = encoder(); + + let without = RequestDisclosureBuilder::build_unsigned( + &enc, + RequestDisclosureParams { + target: address(1), + reason: b"kyc check".to_vec(), + evidence: None, + }, + 0, + ); + + let with_evidence = RequestDisclosureBuilder::build_unsigned( + &enc, + RequestDisclosureParams { + target: address(1), + reason: b"kyc check".to_vec(), + evidence: Some(b"exhibit_a".to_vec()), + }, + 0, + ); + + assert_ne!(without.call_data(), with_evidence.call_data()); + assert!(with_evidence.call_data().len() > without.call_data().len()); + } + + #[test] + fn test_approve_disclosure_payload_is_reflected_in_call_data() { + let enc = encoder(); + + let small_proof = ApproveDisclosureBuilder::build_unsigned( + &enc, + ApproveDisclosureParams { + auditor: address(1), + commitment: commitment(2), + zk_proof: vec![0u8; 32], + disclosed_data: vec![0u8; 8], + }, + 0, + ); + + let large_proof = ApproveDisclosureBuilder::build_unsigned( + &enc, + ApproveDisclosureParams { + auditor: address(1), + commitment: commitment(2), + zk_proof: vec![0u8; 256], + disclosed_data: vec![0u8; 8], + }, + 0, + ); + + assert!( + large_proof.call_data().len() > small_proof.call_data().len(), + "larger proof bytes must produce longer call_data" + ); + } + + #[test] + fn test_reject_disclosure_reason_affects_encoding() { + let enc = encoder(); + + let short = RejectDisclosureBuilder::build_unsigned( + &enc, + RejectDisclosureParams { + auditor: address(1), + reason: b"no".to_vec(), + }, + 0, + ); + + let long = RejectDisclosureBuilder::build_unsigned( + &enc, + RejectDisclosureParams { + auditor: address(1), + reason: b"insufficient documentation provided".to_vec(), + }, + 0, + ); + + assert!( + long.call_data().len() > short.call_data().len(), + "longer reason must produce longer call_data" + ); + } + + #[test] + fn test_reject_disclosure_different_auditors_differ() { + let enc = encoder(); + + let tx_a = RejectDisclosureBuilder::build_unsigned( + &enc, + RejectDisclosureParams { + auditor: address(0xAA), + reason: b"denied".to_vec(), + }, + 0, + ); + let tx_b = RejectDisclosureBuilder::build_unsigned( + &enc, + RejectDisclosureParams { + auditor: address(0xBB), + reason: b"denied".to_vec(), + }, + 0, + ); + + assert_ne!(tx_a.call_data(), tx_b.call_data()); + } + + // ── lifecycle sequence: policy β†’ request β†’ approve β†’ reject ────────────── + + #[test] + fn test_compliance_lifecycle_sequence_produces_distinct_transactions() { + let enc = encoder(); + + let set_policy = SetAuditPolicyBuilder::build_unsigned( + &enc, + SetAuditPolicyParams { + auditors: vec![AuditorInfo { + account: address(1), + public_key: None, + authorized_from: 100, + }], + conditions: vec![DisclosureConditionType::ManualApproval], + max_frequency: Some(24), + }, + 10, + ); + + let request = RequestDisclosureBuilder::build_unsigned( + &enc, + RequestDisclosureParams { + target: address(2), + reason: b"compliance audit Q4".to_vec(), + evidence: Some(b"txid:0xdeadbeef".to_vec()), + }, + 11, + ); + + let approve = ApproveDisclosureBuilder::build_unsigned( + &enc, + ApproveDisclosureParams { + auditor: address(1), + commitment: commitment(3), + zk_proof: vec![0u8; 256], + disclosed_data: vec![0u8; 32], + }, + 12, + ); + + let reject = RejectDisclosureBuilder::build_unsigned( + &enc, + RejectDisclosureParams { + auditor: address(1), + reason: b"duplicate request".to_vec(), + }, + 13, + ); + + // All four transactions are different + let all_data = [ + set_policy.call_data(), + request.call_data(), + approve.call_data(), + reject.call_data(), + ]; + for i in 0..all_data.len() { + for j in (i + 1)..all_data.len() { + assert_ne!( + all_data[i], all_data[j], + "tx {i} and tx {j} should produce distinct call_data" + ); + } + } + + // Nonces increase monotonically + assert_eq!(set_policy.nonce(), 10); + assert_eq!(request.nonce(), 11); + assert_eq!(approve.nonce(), 12); + assert_eq!(reject.nonce(), 13); + } + + // ── public_signals β†’ SubmitDisclosureBuilder full round-trip ───────────── + + #[test] + fn test_submit_disclosure_with_all_fields_disclosed() { + let enc = encoder(); + let commitment_bytes = [0x11u8; 32]; + let value: u64 = 250_000_000; + let asset_id: u32 = 1; + let owner_hash = [0x22u8; 32]; + + let signals = pack_public_signals(&commitment_bytes, value, asset_id, &owner_hash); + assert_eq!(signals.len(), 76); + + // Verify round-trip parsing of each field + assert_eq!(&signals[0..32], &commitment_bytes); + assert_eq!( + u64::from_le_bytes(signals[32..40].try_into().unwrap()), + value + ); + assert_eq!( + u32::from_le_bytes(signals[40..44].try_into().unwrap()), + asset_id + ); + assert_eq!(&signals[44..76], &owner_hash); + + let tx = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: Commitment::from_bytes_unchecked(commitment_bytes), + proof_bytes: vec![0u8; 256], + public_signals: signals, + partial_data: vec![0u8; 32], + auditor: None, + }, + 99, + ); + + assert_eq!(tx.call_data()[0], PALLET_INDEX); + assert_eq!(tx.call_data()[1], compliance_calls::SUBMIT_DISCLOSURE); + assert_eq!(tx.nonce(), 99); + } + + #[test] + fn test_submit_disclosure_value_only_mask_signals_format() { + let enc = encoder(); + // Only value is disclosed; asset_id and owner_hash slots are zeros + let commitment_bytes = [0x33u8; 32]; + let value: u64 = 1_234_567; + let signals = pack_public_signals(&commitment_bytes, value, 0, &[0u8; 32]); + + assert_eq!(&signals[0..32], &commitment_bytes); + assert_eq!( + u64::from_le_bytes(signals[32..40].try_into().unwrap()), + value + ); + assert_eq!(&signals[40..44], &[0u8; 4]); + assert_eq!(&signals[44..76], &[0u8; 32]); + + let tx = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: Commitment::from_bytes_unchecked(commitment_bytes), + proof_bytes: vec![0u8; 256], + public_signals: signals, + partial_data: vec![], + auditor: None, + }, + 0, + ); + assert_eq!(tx.call_data()[1], compliance_calls::SUBMIT_DISCLOSURE); + } + + // ── witness β†’ public_signals integration (requires crypto feature) ──────── + + #[cfg(any(feature = "crypto-zk", feature = "crypto"))] + mod crypto_flow { + use crate::application::disclosure::create_disclosure_witness; + use crate::application::params::SubmitDisclosureParams; + use crate::domain::types::Commitment; + use crate::infrastructure::serializers::SubstrateTransactionEncoder; + use crate::presentation::config::{compliance_calls, PALLET_INDEX}; + use orbinum_encrypted_memo::{DisclosureMask, MemoData}; + + // disclosure_witness_to_public_signals lives in the parent module (disclosure_flow.rs root) + use super::super::disclosure_witness_to_public_signals; + // builders live in the grandparent (compliance) + use super::super::super::SubmitDisclosureBuilder; + + fn enc() -> SubstrateTransactionEncoder { + SubstrateTransactionEncoder::new() + } + + /// For BN254 scalar field, u64_to_field_bytes(v) stores v in LE order, + /// so bytes [0..8] of the field element equal v.to_le_bytes(). + fn field_element_first_u64(fe: &[u8; 32]) -> u64 { + u64::from_le_bytes(fe[0..8].try_into().unwrap()) + } + + fn field_element_first_u32(fe: &[u8; 32]) -> u32 { + u32::from_le_bytes(fe[0..4].try_into().unwrap()) + } + + #[test] + fn test_witness_to_public_signals_has_correct_length() { + let memo = MemoData::new(100, [0u8; 32], [0u8; 32], 1); + let commitment = [0xFFu8; 32]; + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + let signals = disclosure_witness_to_public_signals(&witness); + assert_eq!(signals.len(), 76, "public_signals must be exactly 76 bytes"); + } + + #[test] + fn test_witness_to_public_signals_commitment_slot_matches() { + let commit_bytes = [0x55u8; 32]; + let memo = MemoData::new(50, [0u8; 32], [0u8; 32], 0); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commit_bytes, &mask).unwrap(); + let signals = disclosure_witness_to_public_signals(&witness); + assert_eq!( + &signals[0..32], + &commit_bytes, + "commitment slot must equal the input commitment" + ); + } + + #[test] + fn test_witness_revealed_value_encodes_correctly_in_signals() { + let value: u64 = 42_000; + let memo = MemoData::new(value, [0u8; 32], [0u8; 32], 0); + let commitment = [0u8; 32]; + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + // Field element stores LE value in first 8 bytes + assert_eq!( + field_element_first_u64(&witness.revealed_value), + value, + "field element first 8 bytes must equal the u64 value" + ); + + let signals = disclosure_witness_to_public_signals(&witness); + let decoded = u64::from_le_bytes(signals[32..40].try_into().unwrap()); + assert_eq!( + decoded, value, + "signals[32..40] must decode to original value" + ); + } + + #[test] + fn test_witness_revealed_asset_id_encodes_correctly_in_signals() { + let asset_id: u32 = 7; + let memo = MemoData::new(100, [0u8; 32], [0u8; 32], asset_id); + let commitment = [0u8; 32]; + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: false, + disclose_asset_id: true, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + assert_eq!( + field_element_first_u32(&witness.revealed_asset_id), + asset_id, + "field element first 4 bytes must equal the u32 asset_id" + ); + + let signals = disclosure_witness_to_public_signals(&witness); + let decoded = u32::from_le_bytes(signals[40..44].try_into().unwrap()); + assert_eq!( + decoded, asset_id, + "signals[40..44] must decode to original asset_id" + ); + } + + #[test] + fn test_witness_revealed_owner_hash_in_signals() { + let owner_pk = [0x77u8; 32]; + let memo = MemoData::new(100, owner_pk, [0u8; 32], 0); + let commitment = [0u8; 32]; + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: true, + disclose_asset_id: false, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + assert_ne!( + witness.revealed_owner_hash, [0u8; 32], + "revealed_owner_hash must be non-zero when owner is disclosed" + ); + + let signals = disclosure_witness_to_public_signals(&witness); + assert_eq!( + &signals[44..76], + &witness.revealed_owner_hash, + "signals[44..76] must match revealed_owner_hash" + ); + } + + #[test] + fn test_witness_undisclosed_slots_are_zero_in_signals() { + // Disclose only asset_id; value and owner should be zero + let memo = MemoData::new(9_999, [0xAAu8; 32], [0u8; 32], 5); + let commitment = [0u8; 32]; + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: false, + disclose_asset_id: true, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + let signals = disclosure_witness_to_public_signals(&witness); + + assert_eq!( + &signals[32..40], + &[0u8; 8], + "undisclosed value slot must be zero" + ); + assert_eq!( + &signals[44..76], + &[0u8; 32], + "undisclosed owner slot must be zero" + ); + } + + #[test] + fn test_all_7_masks_produce_valid_submit_disclosure_transactions() { + let enc = enc(); + let memo = MemoData::new(1_000, [0x11u8; 32], [0x22u8; 32], 3); + let commitment_bytes = [0x33u8; 32]; + + let masks = [ + (true, false, false), + (false, true, false), + (false, false, true), + (true, true, false), + (true, false, true), + (false, true, true), + (true, true, true), + ]; + + for (dv, do_, da) in masks { + let mask = DisclosureMask { + disclose_value: dv, + disclose_owner: do_, + disclose_asset_id: da, + disclose_blinding: false, + }; + + let witness = create_disclosure_witness(&memo, &commitment_bytes, &mask) + .expect("witness must succeed for valid mask"); + + let signals = disclosure_witness_to_public_signals(&witness); + assert_eq!(signals.len(), 76); + + let tx = SubmitDisclosureBuilder::build_unsigned( + &enc, + SubmitDisclosureParams { + commitment: Commitment::from_bytes_unchecked(commitment_bytes), + proof_bytes: vec![0u8; 256], + public_signals: signals, + partial_data: vec![], + auditor: None, + }, + 0, + ); + + assert_eq!( + tx.call_data()[0], + PALLET_INDEX, + "mask (dv={dv},do={do_},da={da}): wrong pallet index" + ); + assert_eq!( + tx.call_data()[1], + compliance_calls::SUBMIT_DISCLOSURE, + "mask (dv={dv},do={do_},da={da}): wrong call index" + ); + } + } + + #[test] + fn test_max_u64_value_packs_correctly_into_signals() { + let memo = MemoData::new(u64::MAX, [0u8; 32], [0u8; 32], 0); + let commitment = [0u8; 32]; + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + let signals = disclosure_witness_to_public_signals(&witness); + + let decoded = u64::from_le_bytes(signals[32..40].try_into().unwrap()); + assert_eq!( + decoded, + u64::MAX, + "max u64 must round-trip through public_signals" + ); + } + + #[test] + fn test_zero_asset_id_produces_zero_slot_in_signals() { + // asset_id = 0 (ORB) β†’ field element is [0; 32] β†’ signals[40..44] = [0; 4] + let memo = MemoData::new(100, [0u8; 32], [0u8; 32], 0); + let commitment = [0u8; 32]; + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: false, + disclose_asset_id: true, + disclose_blinding: false, + }; + let witness = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + let signals = disclosure_witness_to_public_signals(&witness); + + assert_eq!( + &signals[40..44], + &[0u8; 4], + "ORB asset_id=0 must produce zero bytes in the asset_id slot" + ); + } + } +} diff --git a/src/application/builders/compliance/mod.rs b/src/application/builders/compliance/mod.rs index 67afaa0..6cf2d46 100644 --- a/src/application/builders/compliance/mod.rs +++ b/src/application/builders/compliance/mod.rs @@ -4,6 +4,7 @@ pub mod approve_disclosure; pub mod audit_policy; pub mod batch_submit_disclosure; +pub mod disclosure_flow; pub mod reject_disclosure; pub mod request_disclosure; pub mod submit_disclosure; diff --git a/src/application/disclosure.rs b/src/application/disclosure.rs new file mode 100644 index 0000000..96b0d0b --- /dev/null +++ b/src/application/disclosure.rs @@ -0,0 +1,687 @@ +//! Selective Disclosure – Witness Builder +//! +//! Builds circuit inputs for the `disclosure.circom` Groth16 circuit. +//! +//! ## Circuit Interface +//! +//! **Public inputs** (auditor sees): +//! - `commitment` – Note commitment (always revealed) +//! - `revealed_value` – Note value, or 0 if not disclosed +//! - `revealed_asset_id` – Asset ID, or 0 if not disclosed +//! - `revealed_owner_hash`– Poseidon(owner_pubkey), or 0 if not disclosed +//! +//! **Private inputs** (prover only): +//! - `value`, `asset_id`, `owner_pubkey`, `blinding` +//! - `viewing_key = Poseidon(owner_pubkey)` β€” NOT the wallet viewing key +//! - `disclose_value`, `disclose_asset_id`, `disclose_owner` (0 or 1) +//! +//! ## Key Design Note +//! +//! The circuit's `viewing_key` is `Poseidon(owner_pubkey)` β€” a ZK-friendly +//! ownership proof. This is distinct from the wallet viewing key (SHA-256 based) +//! used for memo encryption. + +extern crate alloc; + +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +use crate::infrastructure::crypto::ZkCryptoProvider; + +use orbinum_encrypted_memo::{DisclosureMask, MemoData}; + +/// All circuit inputs for `disclosure.circom`. +/// +/// Each field element is stored as 32 little-endian bytes matching the +/// internal arkworks / snarkjs byte representation. +/// +/// The TypeScript consumer (`proof-generator/src/disclosure.ts`) converts +/// these bytes to decimal BigInt strings before calling snarkjs. +#[derive(Debug, Clone)] +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +pub struct DisclosureWitness { + // ── Public inputs (the auditor receives these) ── + /// The note commitment. Always revealed so the auditor can link to the pool. + pub commitment: [u8; 32], + /// `value` if `disclose_value`, else `0`. + pub revealed_value: [u8; 32], + /// `asset_id` if `disclose_asset_id`, else `0`. + pub revealed_asset_id: [u8; 32], + /// `Poseidon(owner_pubkey)` if `disclose_owner`, else `0`. + pub revealed_owner_hash: [u8; 32], + + // ── Private inputs (the prover keeps secret) ── + /// Note value as a field element. + pub value: [u8; 32], + /// Note asset ID as a field element. + pub asset_id: [u8; 32], + /// Owner public key (already a 32-byte field element). + pub owner_pubkey: [u8; 32], + /// Blinding factor (already a 32-byte field element). + pub blinding: [u8; 32], + /// `Poseidon(owner_pubkey)` – proves ownership without revealing spending key. + pub viewing_key: [u8; 32], + /// `true` if the value field will be disclosed. + pub disclose_value: bool, + /// `true` if the asset_id field will be disclosed. + pub disclose_asset_id: bool, + /// `true` if the owner (hash) field will be disclosed. + pub disclose_owner: bool, +} + +/// Error type for witness construction failures. +#[derive(Debug)] +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +pub enum DisclosureError { + /// The DisclosureMask failed validation (e.g., no fields selected, or blinding set). + InvalidMask(&'static str), + /// A Poseidon / field-element conversion error. + CryptoError(alloc::string::String), +} + +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +impl core::fmt::Display for DisclosureError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + DisclosureError::InvalidMask(msg) => write!(f, "Invalid disclosure mask: {msg}"), + DisclosureError::CryptoError(msg) => write!(f, "Crypto error: {msg}"), + } + } +} + +/// Builds all circuit inputs for the selective disclosure proof. +/// +/// # Arguments +/// +/// - `memo` – Decrypted memo data containing the note's private fields. +/// - `commitment` – The 32-byte commitment that anchors the note in the Merkle tree. +/// - `mask` – Which fields to reveal to the auditor. +/// +/// # Returns +/// +/// A [`DisclosureWitness`] ready to be serialized to JSON and passed to snarkjs/groth16-proofs. +/// +/// # Errors +/// +/// Returns [`DisclosureError::InvalidMask`] if the mask is invalid (blinding set, or empty). +/// Returns [`DisclosureError::CryptoError`] if Poseidon or byte conversion fails. +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +pub fn create_disclosure_witness( + memo: &MemoData, + commitment: &[u8; 32], + mask: &DisclosureMask, +) -> Result { + // 1. Validate mask + mask.validate() + .map_err(|_| DisclosureError::InvalidMask("Disclosure mask validation failed"))?; + + let crypto = ZkCryptoProvider::new(); + + // 2. Convert scalar fields to 32-byte field element representations + let value_bytes = ZkCryptoProvider::u64_to_field_bytes(memo.value); + let asset_id_bytes = ZkCryptoProvider::u64_to_field_bytes(memo.asset_id as u64); + + // 3. Compute viewing_key = Poseidon(owner_pubkey) + // This is the circuit's ownership check β€” distinct from the wallet viewing key. + let viewing_key = crypto + .poseidon_hash_1(memo.owner_pk) + .map_err(DisclosureError::CryptoError)?; + + // 4. Compute public (revealed) signals based on mask + let zero = [0u8; 32]; + let revealed_value = if mask.disclose_value { + value_bytes + } else { + zero + }; + let revealed_asset_id = if mask.disclose_asset_id { + asset_id_bytes + } else { + zero + }; + let revealed_owner_hash = if mask.disclose_owner { + viewing_key + } else { + zero + }; + + Ok(DisclosureWitness { + // Public inputs + commitment: *commitment, + revealed_value, + revealed_asset_id, + revealed_owner_hash, + // Private inputs + value: value_bytes, + asset_id: asset_id_bytes, + owner_pubkey: memo.owner_pk, + blinding: memo.blinding, + viewing_key, + disclose_value: mask.disclose_value, + disclose_asset_id: mask.disclose_asset_id, + disclose_owner: mask.disclose_owner, + }) +} + +#[cfg(test)] +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +mod tests { + use super::*; + + fn make_memo(value: u64, asset_id: u32) -> MemoData { + MemoData::new(value, [0x11u8; 32], [0x99u8; 32], asset_id) + } + + fn make_commitment() -> [u8; 32] { + [0x42u8; 32] + } + + // ── Mask validation ────────────────────────────────────────────────────── + + #[test] + fn test_empty_mask_rejected() { + let memo = make_memo(100, 0); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let result = create_disclosure_witness(&memo, &commitment, &mask); + assert!(matches!(result, Err(DisclosureError::InvalidMask(_)))); + } + + #[test] + fn test_blinding_mask_rejected() { + let memo = make_memo(100, 0); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: true, + }; + let result = create_disclosure_witness(&memo, &commitment, &mask); + assert!(matches!(result, Err(DisclosureError::InvalidMask(_)))); + } + + // ── Reveal value only ─────────────────────────────────────────────────── + + #[test] + fn test_disclose_value_only() { + let memo = make_memo(12345, 1); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + assert_eq!(w.commitment, commitment); + assert_ne!(w.revealed_value, [0u8; 32], "value should be revealed"); + assert_eq!(w.revealed_asset_id, [0u8; 32], "asset_id should be hidden"); + assert_eq!(w.revealed_owner_hash, [0u8; 32], "owner should be hidden"); + + assert!(w.disclose_value); + assert!(!w.disclose_asset_id); + assert!(!w.disclose_owner); + } + + // ── Reveal asset_id only ──────────────────────────────────────────────── + + #[test] + fn test_disclose_asset_id_only() { + let memo = make_memo(100, 42); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: false, + disclose_asset_id: true, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + assert_eq!(w.revealed_value, [0u8; 32], "value should be hidden"); + assert_ne!( + w.revealed_asset_id, [0u8; 32], + "asset_id should be revealed" + ); + assert_eq!(w.revealed_owner_hash, [0u8; 32], "owner should be hidden"); + } + + // ── Reveal owner hash only ────────────────────────────────────────────── + + #[test] + fn test_disclose_owner_only() { + let memo = make_memo(100, 1); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: true, + disclose_asset_id: false, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + assert_eq!(w.revealed_value, [0u8; 32], "value should be hidden"); + assert_eq!(w.revealed_asset_id, [0u8; 32], "asset_id should be hidden"); + assert_ne!( + w.revealed_owner_hash, [0u8; 32], + "owner hash should be revealed" + ); + + // viewing_key == revealed_owner_hash when owner is disclosed + assert_eq!(w.viewing_key, w.revealed_owner_hash); + } + + // ── Reveal all fields ─────────────────────────────────────────────────── + + #[test] + fn test_disclose_all_fields() { + let memo = make_memo(999, 7); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: true, + disclose_asset_id: true, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + assert_ne!(w.revealed_value, [0u8; 32]); + assert_ne!(w.revealed_asset_id, [0u8; 32]); + assert_ne!(w.revealed_owner_hash, [0u8; 32]); + assert_eq!(w.viewing_key, w.revealed_owner_hash); + } + + // ── value consistency ─────────────────────────────────────────────────── + + #[test] + fn test_revealed_value_matches_private_value() { + let memo = make_memo(777, 0); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + assert_eq!(w.revealed_value, w.value); + } + + // ── asset_id consistency ───────────────────────────────────────────────── + + #[test] + fn test_revealed_asset_matches_private_asset() { + let memo = make_memo(1, 99); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: false, + disclose_asset_id: true, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + assert_eq!(w.revealed_asset_id, w.asset_id); + } + + // ── zero value edge case ──────────────────────────────────────────────── + + #[test] + fn test_zero_value_disclosed() { + // value=0 is a valid field element; its byte repr is [0;32] + // but the test for "value hidden" uses zero too β€” they happen to match. + // This test verifies the code runs without panic. + let memo = make_memo(0, 0); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask); + assert!(w.is_ok()); + } + + // ── commitment passthrough ───────────────────────────────────────────── + + #[test] + fn test_commitment_passthrough() { + let memo = make_memo(100, 0); + let commitment = [0xABu8; 32]; + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + assert_eq!(w.commitment, commitment); + } + + // ── determinism ───────────────────────────────────────────────────────── + + #[test] + fn test_witness_is_deterministic() { + let memo = make_memo(42, 3); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: true, + disclose_asset_id: true, + disclose_blinding: false, + }; + let w1 = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + let w2 = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + assert_eq!(w1.viewing_key, w2.viewing_key); + assert_eq!(w1.revealed_owner_hash, w2.revealed_owner_hash); + } + + // ── ZkCryptoProvider consistency ───────────────────────────────────────── + + #[test] + fn test_viewing_key_equals_poseidon_of_owner_pubkey() { + // viewing_key must match Poseidon(owner_pubkey) computed independently. + // This verifies the Rust impl matches the circom constraint: + // viewing_key === Poseidon(owner_pubkey) + use crate::infrastructure::crypto::ZkCryptoProvider; + + let owner_pk = [0x55u8; 32]; + let memo = MemoData::new(100, owner_pk, [0x77u8; 32], 0); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: false, + disclose_owner: true, + disclose_asset_id: false, + disclose_blinding: false, + }; + + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + let expected_vk = ZkCryptoProvider::new() + .poseidon_hash_1(owner_pk) + .expect("poseidon_hash_1 should succeed"); + + assert_eq!( + w.viewing_key, expected_vk, + "viewing_key must equal Poseidon(owner_pubkey)" + ); + assert_eq!( + w.revealed_owner_hash, expected_vk, + "revealed_owner_hash must equal Poseidon(owner_pubkey) when owner is disclosed" + ); + } + + #[test] + fn test_viewing_key_differs_from_zero_when_owner_hidden() { + // Even when not disclosing the owner, viewing_key must still be + // computed (non-zero) β€” it is a required private input. + let owner_pk = [0xAAu8; 32]; + let memo = MemoData::new(50, owner_pk, [0xBBu8; 32], 2); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&memo, &make_commitment(), &mask).unwrap(); + + // viewing_key (private input) is always the real Poseidon hash + assert_ne!( + w.viewing_key, [0u8; 32], + "viewing_key must be non-zero even when owner is not disclosed" + ); + // revealed_owner_hash (public input) is zero when not disclosing + assert_eq!( + w.revealed_owner_hash, [0u8; 32], + "revealed_owner_hash must be zero when owner is not disclosed" + ); + } + + #[test] + fn test_real_commitment_passthrough() { + // Compute a real commitment from the note data and verify it is + // forwarded unmodified into the witness. + use crate::infrastructure::crypto::ZkCryptoProvider; + + let value: u64 = 12_345; + let owner_pk = [0x11u8; 32]; + let blinding = [0x22u8; 32]; + let asset_id: u32 = 7; + + let real_commitment = ZkCryptoProvider::new() + .compute_commitment(value as u128, asset_id, owner_pk, blinding) + .expect("commitment should be computable"); + + let memo = MemoData::new(value, owner_pk, blinding, asset_id); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: false, + disclose_blinding: false, + }; + + let w = create_disclosure_witness(&memo, &real_commitment, &mask).unwrap(); + + assert_eq!( + w.commitment, real_commitment, + "witness commitment must equal the real computed commitment" + ); + } + + #[test] + fn test_private_fields_match_memo_data() { + // Every private input in the witness must correspond to the original + // MemoData values (field element representation may differ from raw bytes, + // but same Poseidon commitment must be computable from them). + use crate::infrastructure::crypto::ZkCryptoProvider; + + let value: u64 = 999; + let owner_pk = [0xCCu8; 32]; + let blinding = [0xDDu8; 32]; + let asset_id: u32 = 3; + + let memo = MemoData::new(value, owner_pk, blinding, asset_id); + let commitment = make_commitment(); + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: true, + disclose_asset_id: true, + disclose_blinding: false, + }; + + let w = create_disclosure_witness(&memo, &commitment, &mask).unwrap(); + + // owner_pubkey and blinding pass through as-is (already field element bytes) + assert_eq!(w.owner_pubkey, owner_pk); + assert_eq!(w.blinding, blinding); + + // value and asset_id are encoded as field elements; + // re-encode and compare + let expected_value_fe = ZkCryptoProvider::u64_to_field_bytes(value); + let expected_asset_fe = ZkCryptoProvider::u64_to_field_bytes(asset_id as u64); + assert_eq!(w.value, expected_value_fe); + assert_eq!(w.asset_id, expected_asset_fe); + } + + // ── all valid mask combinations ────────────────────────────────────────── + + #[test] + fn test_all_7_valid_mask_combinations() { + // There are 2^3 = 8 combinations excluding the empty mask β†’ 7 valid. + let memo = make_memo(100, 1); + let commitment = make_commitment(); + + let combinations = [ + (true, false, false), + (false, true, false), + (false, false, true), + (true, true, false), + (true, false, true), + (false, true, true), + (true, true, true), + ]; + + for (dv, do_, da) in combinations { + let mask = DisclosureMask { + disclose_value: dv, + disclose_owner: do_, + disclose_asset_id: da, + disclose_blinding: false, + }; + let result = create_disclosure_witness(&memo, &commitment, &mask); + assert!( + result.is_ok(), + "mask (value={dv}, owner={do_}, asset={da}) should succeed" + ); + + let w = result.unwrap(); + // Zero iff not disclosed + assert_eq!(w.revealed_value != [0u8; 32], dv); + assert_eq!(w.revealed_owner_hash != [0u8; 32], do_); + assert_eq!(w.revealed_asset_id != [0u8; 32], da); + } + } + + // ── full round-trip: encrypt β†’ decrypt β†’ witness ───────────────────────── + + #[test] + #[cfg(feature = "std")] + fn test_encrypt_decrypt_then_build_witness() { + // Full integration: create a memo, encrypt it, decrypt it back, + // then build the disclosure witness from the decrypted data. + use crate::application::memo_utils::{create_encrypted_memo, decrypt_encrypted_memo}; + + let value: u64 = 5_000; + let owner_pk = [0x33u8; 32]; + let blinding = [0x44u8; 32]; + let asset_id: u32 = 7; // non-zero so its field element is also non-zero + let commitment = [0xFFu8; 32]; + let viewing_key = [0x55u8; 32]; + + // Encrypt + let encrypted = create_encrypted_memo( + value, + owner_pk, + blinding, + asset_id, + &commitment, + &viewing_key, + ) + .expect("encryption should succeed"); + + assert_eq!(encrypted.len(), 104, "encrypted memo must be 104 bytes"); + + // Decrypt + let decrypted = decrypt_encrypted_memo(&encrypted, &commitment, &viewing_key) + .expect("decryption should succeed"); + + assert_eq!(decrypted.value, value); + assert_eq!(decrypted.owner_pk, owner_pk); + assert_eq!(decrypted.blinding, blinding); + assert_eq!(decrypted.asset_id, asset_id); + + // Build witness from decrypted data + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: true, + disclose_asset_id: true, + disclose_blinding: false, + }; + + let w = create_disclosure_witness(&decrypted, &commitment, &mask) + .expect("witness from decrypted memo should succeed"); + + // Commitment passthrough + assert_eq!(w.commitment, commitment); + // Private fields match original + assert_eq!(w.owner_pubkey, owner_pk); + assert_eq!(w.blinding, blinding); + // Disclosed: revealed == private input (exact field element equality) + assert_eq!( + w.revealed_value, w.value, + "revealed_value must match private value" + ); + assert_eq!( + w.revealed_asset_id, w.asset_id, + "revealed_asset_id must match private asset_id" + ); + assert_eq!( + w.revealed_owner_hash, w.viewing_key, + "revealed_owner_hash must match viewing_key" + ); + // All three are non-zero since we used non-zero inputs + assert_ne!(w.revealed_value, [0u8; 32]); + assert_ne!(w.revealed_owner_hash, [0u8; 32]); + assert_ne!(w.revealed_asset_id, [0u8; 32]); + } + + #[test] + #[cfg(feature = "std")] + fn test_encrypt_decrypt_witness_with_real_commitment() { + // Full integration using a commitment computed from the actual note data β€” + // the same commitment that would be stored on-chain. + use crate::application::memo_utils::{create_encrypted_memo, decrypt_encrypted_memo}; + use crate::infrastructure::crypto::ZkCryptoProvider; + + let value: u64 = 100_000_000; // 100 ORB (in base units) + let owner_pk = [0x77u8; 32]; + let blinding = [0x88u8; 32]; + let asset_id: u32 = 0; // ORB asset_id is 0 β€” field element == [0;32] + let viewing_key = [0x99u8; 32]; + + // Compute commitment exactly as the protocol does on shield + let real_commitment = ZkCryptoProvider::new() + .compute_commitment(value as u128, asset_id, owner_pk, blinding) + .expect("commitment computation should succeed"); + + // Encrypt and decrypt + let encrypted = create_encrypted_memo( + value, + owner_pk, + blinding, + asset_id, + &real_commitment, + &viewing_key, + ) + .expect("encryption should succeed"); + + let decrypted = decrypt_encrypted_memo(&encrypted, &real_commitment, &viewing_key) + .expect("decryption should succeed"); + + // Build witness + let mask = DisclosureMask { + disclose_value: true, + disclose_owner: false, + disclose_asset_id: true, + disclose_blinding: false, + }; + let w = create_disclosure_witness(&decrypted, &real_commitment, &mask).unwrap(); + + // The witness commitment is the on-chain commitment + assert_eq!(w.commitment, real_commitment); + // Disclosed: revealed == private (even for zero-valued fields) + assert_eq!( + w.revealed_value, w.value, + "revealed_value must match private value" + ); + assert_eq!( + w.revealed_asset_id, w.asset_id, + "revealed_asset_id must match private asset_id" + ); + // value=100_000_000 β†’ non-zero field element + assert_ne!(w.revealed_value, [0u8; 32]); + // asset_id=0 β†’ zero field element (correct β€” ORB asset has id=0) + assert_eq!( + w.revealed_asset_id, [0u8; 32], + "asset_id=0 field element is zero" + ); + assert_eq!(w.revealed_owner_hash, [0u8; 32], "owner not disclosed"); + + // viewing_key is always present (private circuit input) + assert_ne!(w.viewing_key, [0u8; 32]); + } +} diff --git a/src/application/key_manager.rs b/src/application/key_manager.rs index 0202315..47fcee2 100644 --- a/src/application/key_manager.rs +++ b/src/application/key_manager.rs @@ -3,28 +3,32 @@ //! Provides high-level functions for deriving and managing cryptographic keys //! in the Orbinum wallet. -use orbinum_encrypted_memo::{derive_eddsa_key, derive_nullifier_key, derive_viewing_key, KeySet}; +use orbinum_encrypted_memo::{ + derive_eddsa_key_from_spending as derive_eddsa_key, + derive_nullifier_key_from_spending as derive_nullifier_key, + derive_viewing_key_from_spending as derive_viewing_key, KeySet, +}; /// Derives viewing key from spending key. /// /// The viewing key allows reading all transactions but cannot spend funds. /// Safe to share with auditors for compliance purposes. pub fn derive_viewing_key_from_spending(spending_key: &[u8; 32]) -> [u8; 32] { - derive_viewing_key(spending_key) + *derive_viewing_key(spending_key).as_bytes() } /// Derives nullifier key from spending key. /// /// Used to compute nullifiers for spending notes. Must be kept secret. pub fn derive_nullifier_key_from_spending(spending_key: &[u8; 32]) -> [u8; 32] { - derive_nullifier_key(spending_key) + *derive_nullifier_key(spending_key).as_bytes() } /// Derives EdDSA signing key from spending key. /// /// Used for circuit signatures in ZK proofs. Must be kept secret. pub fn derive_eddsa_key_from_spending(spending_key: &[u8; 32]) -> [u8; 32] { - derive_eddsa_key(spending_key) + *derive_eddsa_key(spending_key).as_bytes() } /// Derives complete keyset from spending key. diff --git a/src/application/memo_utils.rs b/src/application/memo_utils.rs index 5829e10..f71bac9 100644 --- a/src/application/memo_utils.rs +++ b/src/application/memo_utils.rs @@ -6,9 +6,10 @@ extern crate alloc; use alloc::vec::Vec; -use orbinum_encrypted_memo::{ - decrypt_memo, encrypt_memo_random, is_valid_encrypted_memo, MemoData, -}; +use orbinum_encrypted_memo::{decrypt_memo, is_valid_encrypted_memo, MemoData}; + +#[cfg(any(feature = "std", target_arch = "wasm32"))] +use orbinum_encrypted_memo::encrypt_memo_random; /// Encrypts memo data for a recipient with automatic random nonce generation. /// @@ -25,6 +26,7 @@ use orbinum_encrypted_memo::{ /// /// # Returns /// Encrypted memo bytes (104 bytes: 12-byte nonce + 92-byte ciphertext) +#[cfg(any(feature = "std", target_arch = "wasm32"))] pub fn create_encrypted_memo( value: u64, owner_pk: [u8; 32], @@ -66,6 +68,7 @@ pub fn validate_encrypted_memo(encrypted: &[u8]) -> bool { /// Creates a dummy encrypted memo for testing. /// /// Returns a valid 104-byte encrypted memo with zero values. +#[cfg(any(feature = "std", target_arch = "wasm32"))] pub fn create_dummy_encrypted_memo() -> Vec { let memo = MemoData::new(0, [0u8; 32], [0u8; 32], 0); let commitment = [0u8; 32]; @@ -80,6 +83,7 @@ mod tests { use super::*; #[test] + #[cfg(feature = "std")] fn test_encrypt_decrypt_roundtrip() { let value = 1000u64; let owner_pk = [1u8; 32]; @@ -110,6 +114,7 @@ mod tests { } #[test] + #[cfg(feature = "std")] fn test_validate_encrypted_memo() { let dummy = create_dummy_encrypted_memo(); assert!(validate_encrypted_memo(&dummy)); @@ -128,6 +133,7 @@ mod tests { } #[test] + #[cfg(feature = "std")] fn test_dummy_encrypted_memo() { let dummy = create_dummy_encrypted_memo(); assert_eq!(dummy.len(), 104); diff --git a/src/application/mod.rs b/src/application/mod.rs index b8b1091..86fde23 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -35,6 +35,12 @@ pub use key_manager::*; pub use memo_utils::*; pub use note_manager::*; +// Selective Disclosure +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +pub mod disclosure; +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +pub use disclosure::{create_disclosure_witness, DisclosureError, DisclosureWitness}; + #[cfg(test)] mod tests { use super::*; diff --git a/src/infrastructure/crypto.rs b/src/infrastructure/crypto.rs index fc45016..a04f4c3 100644 --- a/src/infrastructure/crypto.rs +++ b/src/infrastructure/crypto.rs @@ -8,6 +8,10 @@ use crate::domain::ports::{HashPort, SignerError, SignerPort}; #[cfg(any(feature = "crypto-signing", feature = "crypto"))] use crate::domain::types::*; +#[cfg(any(feature = "crypto-signing", feature = "crypto"))] +use blake2::digest::consts::U32; +#[cfg(any(feature = "crypto-signing", feature = "crypto"))] +use blake2::{Blake2b, Digest}; #[cfg(any(feature = "crypto-signing", feature = "crypto"))] use libsecp256k1::{Message, PublicKey as Secp256k1PublicKey, SecretKey as Secp256k1SecretKey}; #[cfg(any(feature = "crypto-signing", feature = "crypto"))] @@ -16,10 +20,12 @@ use tiny_keccak::{Hasher, Keccak}; // Imports for ZK operations (Poseidon, commitments) #[cfg(any(feature = "crypto-zk", feature = "crypto"))] use orbinum_zk_core::{ - domain::ports::PoseidonHasher, Blinding, FieldElement, LightPoseidonHasher, Note, NoteDto, - OwnerPubkey, + domain::ports::PoseidonHasher, poseidon_hash_1 as zk_poseidon_hash_1, Blinding, FieldElement, + LightPoseidonHasher, Note, NoteDto, OwnerPubkey, }; +use alloc::string::String; + /// ZK cryptography provider using orbinum-zk-core. #[cfg(any(feature = "crypto-zk", feature = "crypto"))] pub struct ZkCryptoProvider; @@ -31,7 +37,7 @@ impl ZkCryptoProvider { } /// Converts bytes to FieldElement using NoteDto roundtrip. - fn bytes_to_field(bytes: [u8; 32]) -> Result { + pub(crate) fn bytes_to_field(bytes: [u8; 32]) -> Result { // Create DTO with bytes in owner_pubkey field let dto = NoteDto::new(0, 0, bytes, [0u8; 32]); // Convert to domain (validates and converts) @@ -41,7 +47,7 @@ impl ZkCryptoProvider { } /// Converts FieldElement to bytes using NoteDto roundtrip. - fn field_to_bytes(field: FieldElement) -> [u8; 32] { + pub(crate) fn field_to_bytes(field: FieldElement) -> [u8; 32] { // Create Note with field in owner_pubkey // Use from_u64(0) instead of zero() to be safe if zero() is not exposed on Blinding let note = Note::new( @@ -127,6 +133,23 @@ impl ZkCryptoProvider { Ok(Self::field_to_bytes(result)) } + + /// Computes Poseidon(1) of a single 32-byte input. + /// + /// Used to derive `viewing_key = Poseidon(owner_pubkey)` for the disclosure circuit. + /// This matches `Poseidon(1)` from circom/circomlibjs. + pub fn poseidon_hash_1(&self, input: [u8; 32]) -> Result<[u8; 32], String> { + let input_field = Self::bytes_to_field(input)?; + let result = zk_poseidon_hash_1(input_field); + Ok(Self::field_to_bytes(result)) + } + + /// Converts a u64 value to a 32-byte field element representation. + /// + /// Used to encode `value` and `asset_id` as circuit field elements. + pub(crate) fn u64_to_field_bytes(value: u64) -> [u8; 32] { + Self::field_to_bytes(FieldElement::from_u64(value)) + } } /// Keccak256 hash implementation. @@ -154,18 +177,24 @@ pub struct EcdsaSigner { #[cfg(any(feature = "crypto-signing", feature = "crypto"))] impl EcdsaSigner { - /// Crea un signer desde una clave privada + /// Creates a signer from a private key pub fn from_secret_key(secret: &SecretKey) -> Result { let secret_key = Secp256k1SecretKey::parse_slice(secret.as_bytes()) .map_err(|_| SignerError::InvalidKey)?; let public_key = Secp256k1PublicKey::from_secret_key(&secret_key); - // Derivar direcciΓ³n Ethereum: keccak256(pubkey)[12..] - let pubkey_bytes = public_key.serialize(); - let hash = Keccak256Hasher::keccak256(&pubkey_bytes[1..]); // Sin el prefijo 0x04 - let address = - Address::from_slice(&hash.as_bytes()[12..]).map_err(|_| SignerError::InvalidKey)?; + // Derive AccountId32: blake2_256(compressed_pubkey_33_bytes) + // This matches Substrate’s derivation for ECDSA accounts: + // AccountId32 = blake2_256(compressed_secp256k1_pubkey) + let pubkey_uncompressed = public_key.serialize(); // [u8; 65]: 0x04 || x || y + let y_odd = pubkey_uncompressed[64] & 1; + let prefix = if y_odd == 0 { 0x02u8 } else { 0x03u8 }; + let mut compressed = [0u8; 33]; + compressed[0] = prefix; + compressed[1..].copy_from_slice(&pubkey_uncompressed[1..33]); + let account_id: [u8; 32] = Blake2b::::digest(&compressed).into(); + let address = Address(account_id); Ok(EcdsaSigner { secret_key, diff --git a/src/lib.rs b/src/lib.rs index 4aea143..6bb7e9c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,12 +70,9 @@ use ::core::fmt; use ::core::result; use alloc::string::String; -// Panic handler for no_std #[cfg(not(feature = "std"))] -#[panic_handler] -fn panic(_info: &core::panic::PanicInfo) -> ! { - loop {} -} +#[allow(unused_imports)] +use alloc::format; // Global allocator for no_std (WASM will use wasm allocator) #[cfg(all(not(feature = "std"), target_arch = "wasm32"))] diff --git a/src/presentation/wasm_bindings.rs b/src/presentation/wasm_bindings.rs index a47b6ee..b61fc53 100644 --- a/src/presentation/wasm_bindings.rs +++ b/src/presentation/wasm_bindings.rs @@ -1,5 +1,7 @@ //! WASM Bindings for JavaScript/TypeScript +use alloc::string::{String, ToString}; +use alloc::vec::Vec; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -752,6 +754,121 @@ impl Crypto { Ok(hash.to_vec()) } + + /// Builds all circuit inputs for the selective disclosure proof. + /// + /// Returns a JSON object with the following fields (all as hex strings): + /// - `commitment`, `revealed_value`, `revealed_asset_id`, `revealed_owner_hash` + /// (public inputs – what the auditor receives) + /// - `value`, `asset_id`, `owner_pubkey`, `blinding`, `viewing_key` + /// (private inputs – kept secret by the prover) + /// - `disclose_value`, `disclose_asset_id`, `disclose_owner` (booleans as 0/1 strings) + /// + /// The TypeScript consumer converts hex values to BigInt decimal strings before + /// passing them to snarkjs / groth16-proofs. + /// + /// # Parameters + /// + /// - `value` – Note value (u64 as decimal string, e.g. `"100000000"`) + /// - `owner_pk` – Owner public key (32 bytes) + /// - `blinding` – Blinding factor (32 bytes) + /// - `asset_id` – Asset ID (u32) + /// - `commitment` – Note commitment (32 bytes) + /// - `disclose_value` – Whether to reveal the value + /// - `disclose_owner` – Whether to reveal the owner hash + /// - `disclose_asset_id` – Whether to reveal the asset ID + #[cfg(any(feature = "crypto-zk", feature = "crypto"))] + #[wasm_bindgen(js_name = buildDisclosureInputs)] + pub fn build_disclosure_inputs( + value: String, + owner_pk: Vec, + blinding: Vec, + asset_id: u32, + commitment: Vec, + disclose_value: bool, + disclose_owner: bool, + disclose_asset_id: bool, + ) -> Result { + use crate::application::disclosure::create_disclosure_witness; + use orbinum_encrypted_memo::{DisclosureMask, MemoData}; + + // Parse value + let value_u64: u64 = value + .parse() + .map_err(|_| JsValue::from_str("Invalid value: must be a u64 decimal string"))?; + + // Validate byte slices + if owner_pk.len() != 32 { + return Err(JsValue::from_str("owner_pk must be 32 bytes")); + } + if blinding.len() != 32 { + return Err(JsValue::from_str("blinding must be 32 bytes")); + } + if commitment.len() != 32 { + return Err(JsValue::from_str("commitment must be 32 bytes")); + } + + let mut owner_pk_bytes = [0u8; 32]; + let mut blinding_bytes = [0u8; 32]; + let mut commitment_bytes = [0u8; 32]; + owner_pk_bytes.copy_from_slice(&owner_pk); + blinding_bytes.copy_from_slice(&blinding); + commitment_bytes.copy_from_slice(&commitment); + + // Build domain types + let memo = MemoData::new(value_u64, owner_pk_bytes, blinding_bytes, asset_id); + let mask = DisclosureMask { + disclose_value, + disclose_owner, + disclose_asset_id, + disclose_blinding: false, // MUST always be false + }; + + // Build witness + let w = create_disclosure_witness(&memo, &commitment_bytes, &mask) + .map_err(|e| JsValue::from_str(&format!("{e}")))?; + + // Helper: bytes to "0x{hex}" string + fn to_hex(b: &[u8; 32]) -> String { + let mut s = String::with_capacity(66); + s.push_str("0x"); + for byte in b { + s.push_str(&format!("{byte:02x}")); + } + s + } + + // Serialize to JS object + let obj = js_sys::Object::new(); + macro_rules! set_hex { + ($key:expr, $val:expr) => { + js_sys::Reflect::set(&obj, &$key.into(), &to_hex($val).into())?; + }; + } + macro_rules! set_bool { + ($key:expr, $val:expr) => { + js_sys::Reflect::set(&obj, &$key.into(), &(if $val { "1" } else { "0" }).into())?; + }; + } + + // Public inputs + set_hex!("commitment", &w.commitment); + set_hex!("revealed_value", &w.revealed_value); + set_hex!("revealed_asset_id", &w.revealed_asset_id); + set_hex!("revealed_owner_hash", &w.revealed_owner_hash); + + // Private inputs + set_hex!("value", &w.value); + set_hex!("asset_id", &w.asset_id); + set_hex!("owner_pubkey", &w.owner_pubkey); + set_hex!("blinding", &w.blinding); + set_hex!("viewing_key", &w.viewing_key); + set_bool!("disclose_value", w.disclose_value); + set_bool!("disclose_asset_id", w.disclose_asset_id); + set_bool!("disclose_owner", w.disclose_owner); + + Ok(obj.into()) + } } /// WASM bindings for encrypted memo operations From 9c0175db803c05c3131ae7b8536776df8e07276a Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:34:37 -0300 Subject: [PATCH 5/9] feat: update version to 0.2.0, enhance README, and add new features including selective disclosure witness API and hybrid CI build --- CHANGELOG.md | 25 +++++++++++++ Cargo.toml | 2 +- README.md | 104 +++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 102 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82aa52..18d9777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `Cargo.toml`: added `path` dependency on local `orbinum-zk-core` to access `poseidon_hash_1` + +### Added +- **Universal NPM Package**: Support for both Node.js (CommonJS/fs) and Web (ESM/fetch) environments in a single package. +- **Hybrid CI Build**: `release.yml` now generates both `pkg/node` and `pkg/web` artifacts. +- **Conditional Exports**: `package.json` configured with `exports` field to automatically select the correct build based on the environment (`browser` vs `node`). +- **Documentation**: Updated README with instructions for JS/TS (Universal), Rust Native, and WASM Runtimes. +- **Selective Disclosure Witness API** (`src/application/disclosure.rs`): + - `create_disclosure_witness()` β€” builds circuit inputs for `disclosure.circom` Groth16 proof + - `DisclosureWitness` struct with 4 public inputs + 7 private inputs as `[u8; 32]` field elements and disclosure flags + - `DisclosureError` enum (`InvalidMask`, `CryptoError`) + - Support for all 7 valid mask combinations (value, asset_id, owner hash) +- **`poseidon_hash_1()`** added to `ZkCryptoProvider` (single-input Poseidon hash for viewing key) +- **WASM binding** `Crypto::buildDisclosureInputs()` exported to JavaScript/TypeScript +- **18 new tests** (11 unit + 7 integration) in `src/application/disclosure.rs` β€” all pass +- **Criterion benchmark** `bench_build_disclosure_witness` β€” ~12.7 Β΅s (release profile) +- **E2E test** `tests/disclosure.test.ts`: full encrypt β†’ decrypt β†’ witness β†’ proof β†’ verify flow (1034ms) +- **E2E test** `tests/shield-and-disclose.test.ts`: Alice shields, generates ZK disclosure proof, Bob verifies (379ms) + +### Changed +- Refactored `release.yml` to support multi-target WASM compilation. +- Updated `Cargo.toml` version to `0.2.0`. + ## [0.1.0] - 2026-02-16 ### Added diff --git a/Cargo.toml b/Cargo.toml index 82eb904..7755f4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orbinum-protocol-core" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "Apache-2.0 OR GPL-3.0-or-later" description = "Core protocol primitives and WASM bindings for interacting with Orbinum" diff --git a/README.md b/README.md index e4173a1..ec1cd89 100644 --- a/README.md +++ b/README.md @@ -2,47 +2,94 @@ Core Rust/WASM protocol library for building and encoding Orbinum shielded-pool transactions. -`orbinum-protocol-core` provides a clean, feature-gated API for: -- unsigned call-data construction, -- signed extrinsic building, -- compliance/disclosure transaction flows, -- ZK helper operations (commitment, nullifier, Poseidon hashing), -- JavaScript/TypeScript consumption through WASM bindings. - -## Status - -- Version: `0.1.0` (initial release) -- Maturity: active development (MVP hardening phase) -- Runtime scope: Substrate/Frontier-compatible transaction payloads +**Universal Package**: Works seamlessly in **Node.js**, **Browsers**, **Rust Native**, and **WASM Runtimes** (Polkadot, Near, etc.). ## Features -- `std` (default) -- `crypto-zk` -- `crypto-signing` -- `crypto` (enables both `crypto-zk` and `crypto-signing`) -- `subxt-native` -- `subxt-web` +- **Universal WASM**: Single package supports `require('fs')` in Node and `fetch()` in Web. +- **Native Rust**: Full support for `no_std` environments and native binary compilation. +- **Type-safe**: Generated from runtime metadata using Subxt. +- **Zero-Knowledge Primitives**: Poseidon hashing, commitment generation, nullifier computation. +- **Transaction Building**: Create unsigned transactions for Note Transfer, Shielding, and Unshielding. +- **Clean Architecture**: Domain-driven design with clear separation of concerns. + +## Installation -## Build +### JavaScript / TypeScript (npm) ```bash -cargo build --release --features crypto +npm install @orbinum/protocol-core ``` -## WASM +### Rust (Cargo) -```bash -wasm-pack build --target web --out-dir pkg --release --features crypto-zk -wasm-pack build --target nodejs --out-dir pkg-node --release --features crypto-zk +```toml +[dependencies] +orbinum-protocol-core = "0.2.0" +``` + +## Usage + +### Web / Browser (React, Next.js) + +The npm package automatically uses `fetch` to load the WASM binary. + +```typescript +import { Crypto, TransactionBuilder } from '@orbinum/protocol-core'; + +async function main() { + // Initialize WASM (downloads from CDN or local asset) + await Crypto.init(); + + // Create a Note Commitment + const commitment = Crypto.computeCommitment("100", 1, ownerPubkey, blinding); + console.log("Commitment:", commitment); +} ``` -## Validate +### Node.js (Backend) + +The npm package automatically uses `fs` to load the WASM binary. + +```javascript +const { Crypto } = require('@orbinum/protocol-core'); + +async function main() { + // Initialize WASM (loads from disk) + await Crypto.init(); + + const hash = Crypto.poseidonHash2(left, right); + console.log("Hash:", hash); +} +``` + +### Rust (Native / WASM Runtime) + +Ideal for CLI tools, Substrate pallets, or generic WASM actors. + +```rust +use orbinum_protocol_core::{CryptoApi, TransactionBuilder}; + +fn main() { + // Native Rust code (no WASM initialization needed) + let commitment = CryptoApi::compute_commitment( + "100", + 1, + &owner_pubkey, + &blinding + ).unwrap(); + + println!("Commitment: {:?}", commitment); +} +``` + +## Build from Source + +To build the universal package locally (requires `rust`, `wasm-pack`, and `node`): ```bash -make fmt -make test -make bench +# Build both targets (pkg/web and pkg/node) +make wasm-all ``` ## Documentation @@ -55,3 +102,4 @@ This README is intentionally brief. Full documentation is in `docs/`: ## License Apache-2.0 OR GPL-3.0-or-later + From 59e6dfc5457cbf1ae2f771e23428a7c79a396c9e Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:35:03 -0300 Subject: [PATCH 6/9] feat: remove outdated API and architecture documentation --- docs/PROTOCOL_CORE_API.md | 219 ----------------------------- docs/PROTOCOL_CORE_ARCHITECTURE.md | 122 ---------------- 2 files changed, 341 deletions(-) delete mode 100644 docs/PROTOCOL_CORE_API.md delete mode 100644 docs/PROTOCOL_CORE_ARCHITECTURE.md diff --git a/docs/PROTOCOL_CORE_API.md b/docs/PROTOCOL_CORE_API.md deleted file mode 100644 index a943fb8..0000000 --- a/docs/PROTOCOL_CORE_API.md +++ /dev/null @@ -1,219 +0,0 @@ -# Protocol Core API Reference - -## Alcance - -Este documento cubre la superficie pΓΊblica expuesta por `orbinum-protocol-core` para consumo: - -- Rust (API principal), -- features condicionales, -- y WASM (bindings para JS/TS). - -Cuando una API depende de un feature, se indica explΓ­citamente. - -## 1. Exportaciones pΓΊblicas en `lib.rs` - -### Reexports principales - -- `pub mod domain` -- `pub mod application` -- `pub mod infrastructure` -- `pub mod presentation` -- `pub use presentation::api::*` -- `pub use application::validators::TransactionValidator` -- `pub use infrastructure::crypto::*` (feature-gated por mΓ³dulo) -- `pub type Result = core::result::Result` -- `pub enum Error` (errores de validaciΓ³n, serializaciΓ³n, cryptography, etc.) - -### Exportaciones WASM (solo `target_arch = "wasm32"`) - -- `pub use presentation::wasm_bindings::*` - -## 2. API Rust recomendada (`presentation::api`) - -## `TransactionApi` - -Constructor: - -- `TransactionApi::new() -> Self` - -### Core builders - -- `build_shield_call_data(params: ShieldParams) -> Result>` -- `build_unshield_call_data(params: UnshieldParams) -> Result>` -- `build_transfer_call_data(params: TransferParams) -> Result>` - -### Compliance builders - -- `build_disclose_call_data(params: DiscloseParams) -> Result>` -- `build_register_vk_call_data(params: RegisterVkParams) -> Result>` -- `build_pause_call_data(params: PauseParams) -> Result>` -- `build_unpause_call_data(params: UnpauseParams) -> Result>` - -### Extrinsics (unsigned) - -- `build_extrinsic(call_data: &[u8]) -> Result>` -- `build_batch_extrinsic(call_datas: &[Vec], atomic: bool) -> Result>` -- `build_batch_calls(call_datas: &[Vec], atomic: bool) -> Result>` - -### Utilidades - -- `validate_transaction(tx: &UnsignedTransaction) -> Result<()>` - -## `SigningApi` (requiere `feature = "crypto"`) - -Constructor: - -- `SigningApi::new() -> Self` - -MΓ©todos: - -- `build_and_sign_shield(params: ShieldParams, private_key: &[u8; 32], nonce: u32) -> Result>` -- `build_and_sign_unshield(params: UnshieldParams, private_key: &[u8; 32], nonce: u32) -> Result>` -- `build_and_sign_transfer(params: TransferParams, private_key: &[u8; 32], nonce: u32) -> Result>` -- `sign_call_data(call_data: &[u8], private_key: &[u8; 32], nonce: u32) -> Result>` -- `derive_public_key(private_key: &[u8; 32]) -> Result<[u8; 33]>` - -## `CryptoApi` (feature-gated) - -La API criptogrΓ‘fica de alto nivel se expone en `presentation::crypto_api` y depende de features crypto. -Incluye utilidades para: - -- generaciΓ³n de keypairs, -- commitments, -- nullifiers, -- hashing Poseidon, -- cifrado/descifrado de memo. - -(Para consumidores de frontend, la vΓ­a recomendada es usar las clases WASM listadas mΓ‘s abajo.) - -## 3. ParΓ‘metros de entrada (`application::params`) - -Tipos de params usados por `TransactionApi`/`SigningApi`: - -- `ShieldParams` -- `UnshieldParams` -- `TransferParams` -- `DiscloseParams` -- `RegisterVkParams` -- `PauseParams` -- `UnpauseParams` - -Estos structs concentran el contrato de entrada para construcciΓ³n de calls/extrinsics. - -## 4. Modelos de salida y tipos comunes - -Tipos comΓΊnmente consumidos: - -- `UnsignedTransaction` -- `SignedTransaction` -- `Address`, `Hash`, `Commitment`, `Nullifier`, `AssetId` -- modelos ZK de `presentation::zk_models` (segΓΊn flujo y feature) - -## 5. API WASM (JS/TS) - -Disponible cuando se compila para `wasm32` y se consume desde el paquete npm generado. - -## Clases principales - -### `TransactionBuilder` - -Operaciones de construcciΓ³n de transacciones/calls para shield, unshield y transfer. -Devuelve bytes serializados para envΓ­o/firma segΓΊn mΓ©todo. - -### `Crypto` - -Operaciones criptogrΓ‘ficas para ZK: - -- derivaciΓ³n/generaciΓ³n de claves, -- cΓ‘lculo de commitment/nullifier, -- utilidades hash. - -### `EncryptedMemo` - -Manejo de memo encriptado: - -- creaciΓ³n, -- serializaciΓ³n, -- parsing, -- cifrado/descifrado. - -### `Proof` - -Wrapper para datos de proof y serializaciΓ³n asociada. - -### `KeyManager` - -Utilidades para gestionar llaves en contexto WASM/JS. - -## 6. Matriz de disponibilidad - -- Core call builders (`TransactionApi`): sin feature crypto obligatorio. -- Signing (`SigningApi`): requiere `crypto`. -- Crypto ZK (`CryptoApi`/`Crypto`): requiere `crypto-zk` o `crypto`. -- WASM classes: requieren build target `wasm32`. - -## 7. Ejemplos de uso - -## Rust: construir call de shield - -```rust -use orbinum_protocol_core::{TransactionApi, ShieldParams}; - -let api = TransactionApi::new(); -let params = ShieldParams { - amount: 1_000, - asset_id: 0, - owner_pubkey: [0u8; 32], - blinding: [1u8; 32], - memo: None, -}; - -let call_data = api.build_shield_call_data(params)?; -``` - -## Rust: construir y firmar (feature `crypto`) - -```rust -use orbinum_protocol_core::{SigningApi, TransferParams}; - -let api = SigningApi::new(); -let private_key = [7u8; 32]; -let nonce = 1; - -let params = TransferParams { - root: [0u8; 32], - input_nullifiers: vec![[0u8; 32]], - output_commitments: vec![[0u8; 32]], - proof: vec![0u8; 192], - memo: None, -}; - -let signed_ext = api.build_and_sign_transfer(params, &private_key, nonce)?; -``` - -## TypeScript (WASM): inicializaciΓ³n y uso bΓ‘sico - -```ts -import init, { TransactionBuilder, Crypto } from "orbinum-protocol-core"; - -await init(); - -const builder = new TransactionBuilder(); -const crypto = new Crypto(); - -// ejemplo conceptual: construir call + operaciΓ³n crypto -// los nombres exactos de mΓ©todos dependen del binding generado en pkg/*.d.ts -``` - -## 8. GuΓ­a de adopciΓ³n - -- Si construyes backend Rust: empieza por `TransactionApi`. -- Si ademΓ‘s firmas en servidor: habilita `crypto` y usa `SigningApi`. -- Si construyes frontend/web wallet: usa build WASM + bindings de `pkg/`. -- Evita acoplarte a mΓ³dulos internos de infraestructura salvo necesidad real. - -## 9. Estabilidad y versionado - -- La API de `presentation::api` es la superficie de consumo prioritaria. -- MΓ³dulos internos (`application`/`infrastructure`) son pΓΊblicos pero pueden evolucionar con menor estabilidad. -- Para integraciones externas, anclar versiΓ³n semver del crate/paquete npm. diff --git a/docs/PROTOCOL_CORE_ARCHITECTURE.md b/docs/PROTOCOL_CORE_ARCHITECTURE.md deleted file mode 100644 index 0d939a3..0000000 --- a/docs/PROTOCOL_CORE_ARCHITECTURE.md +++ /dev/null @@ -1,122 +0,0 @@ -# Protocol Core Architecture - -## Objetivo - -`orbinum-protocol-core` es el nΓΊcleo de construcciΓ³n de transacciones y utilidades criptogrΓ‘ficas para Orbinum, con soporte para Rust nativo y WASM. - -EstΓ‘ organizado siguiendo Clean Architecture para separar: - -- reglas de negocio, -- casos de uso, -- adaptadores tΓ©cnicos, -- y superficie pΓΊblica. - -## Estructura por capas - -```text -src/ -β”œβ”€β”€ domain/ # Tipos puros, entidades y puertos (sin infraestructura) -β”œβ”€β”€ application/ # Builders, validadores, params y lΓ³gica de orquestaciΓ³n -β”œβ”€β”€ infrastructure/ # Implementaciones tΓ©cnicas (codec, serializers, crypto, adapters) -└── presentation/ # API pΓΊblica Rust/WASM y modelos de salida -``` - -### 1) Domain (`src/domain`) - -Responsabilidad: - -- Modelo de dominio y contratos. -- No depende de detalles concretos de serializaciΓ³n ni del runtime. - -Contenido principal: - -- `types/`: `Address`, `Hash`, `Commitment`, `Nullifier`, `AssetId`, etc. -- `entities.rs`: `UnsignedTransaction`, `SignedTransaction`. -- `ports.rs` y `ports/encoder.rs`: contratos como `SignerPort`, `HashPort`, `EncoderPort`. - -### 2) Application (`src/application`) - -Responsabilidad: - -- Casos de uso y validaciones de negocio. -- Usa puertos del dominio y params de entrada. - -Contenido principal: - -- `builders/`: shield, unshield, transfer, compliance, extrinsic. -- `validators/`: validaciΓ³n de transacciones core y compliance. -- `params.rs`: contratos de entrada para todos los builders/APIs. -- utilidades: `memo_utils`, `key_manager`, `note_manager`. - -### 3) Infrastructure (`src/infrastructure`) - -Responsabilidad: - -- Implementaciones concretas de puertos. -- SerializaciΓ³n SCALE, adapters serde y crypto real. - -Contenido principal: - -- `codec/`: wrappers y encoder SCALE. -- `serializers/`: construcciΓ³n de call data y serializaciΓ³n de extrinsics. -- `serde_adapters/`: serializaciΓ³n/deserializaciΓ³n de tipos de dominio. -- `crypto.rs`: provider ZK y signer ECDSA/Keccak (segΓΊn features). - -### 4) Presentation (`src/presentation`) - -Responsabilidad: - -- Superficie pΓΊblica de consumo. -- API orientada a uso (Rust y WASM). - -Contenido principal: - -- `api/`: `TransactionApi`, `SigningApi`. -- `crypto_api.rs`: operaciones ZK de alto nivel. -- `zk_models.rs`: modelos pΓΊblicos para note/nullifier/proofs. -- `wasm_bindings.rs`: bindings JS/TS cuando el target es wasm32. -- `config.rs`: constantes de Γ­ndices de pallet/calls. - -## Flujo principal de ejecuciΓ³n - -```text -Cliente (Rust o WASM) - -> presentation/api - -> application/builders + validators - -> infrastructure/serializers + codec - -> bytes SCALE listos para firmar/enviar -``` - -Para flujos con firma integrada (nativo): - -```text -presentation::api::SigningApi - -> infrastructure::crypto::EcdsaSigner - -> application::builders::{shield,unshield,transfer}::build_signed - -> extrinsic serializado -``` - -## Feature gates y targets - -- `crypto-zk`: habilita operaciones ZK (commitment/nullifier/poseidon). -- `crypto-signing`: habilita firma ECDSA/Keccak. -- `crypto`: habilita ambas (`crypto-zk` + `crypto-signing`). -- `target_arch = "wasm32"`: expone `presentation::wasm_bindings`. - -Notas: - -- `SigningApi` requiere `feature = "crypto"`. -- `wasm_bindings` solo existe para target WASM. - -## Principios aplicados - -- SeparaciΓ³n estricta de responsabilidades por capa. -- Dependencias dirigidas hacia adentro (presentation -> application -> domain). -- Infraestructura implementa contratos definidos por domain/application. -- Surface API estable concentrada en `presentation` y reexports de `lib.rs`. - -## RecomendaciΓ³n de uso - -- Integraciones de producto: usar `presentation::api` (y WASM bindings en frontend). -- Integraciones avanzadas: usar capas inferiores solo si necesitas control fino. -- Mantener cambios de serializaciΓ³n/crypto en `infrastructure` para no contaminar dominio. From e1e0e151f9089c5fb01c336edaf28e1a2b630801 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:37:56 -0300 Subject: [PATCH 7/9] feat: remove deprecated build-web target and update wasm-all target in Makefile --- Makefile | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index ee94cd9..ac35a44 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build test check clean wasm wasm-node examples doc help +.PHONY: all build test check clean wasm wasm-node wasm-all examples doc help # Default target all: check test @@ -30,15 +30,6 @@ clean: @rm -rf pkg pkg-node target @echo "βœ… Clean complete" -# Build for web (with ZK crypto, no signing) -build-web: - @echo "🌐 Building for web (with crypto-zk)..." - @wasm-pack build --target web --out-dir pkg --release --features crypto-zk - @echo "βœ… Web build complete: pkg/" - @echo " βœ… Poseidon hash available" - @echo " βœ… Commitments/Nullifiers available" - @echo " ❌ Signing NOT available (use @polkadot/keyring)" - # Build WASM for web (with ZK crypto, no signing) wasm: @echo "🌐 Building WASM for web (with crypto-zk)..." @@ -55,7 +46,7 @@ wasm-node: @echo "βœ… WASM Node build complete: pkg-node/" # Build all WASM targets -wasm-all: build-web wasm wasm-node +wasm-all: wasm wasm-node # Format code fmt: @@ -77,8 +68,7 @@ help: @echo " make test - Run all tests" @echo " make check - Check code and run clippy" @echo " make clean - Clean build artifacts" - @echo " make build-web - Build for web" - @echo " make wasm - Build WASM for web" + @echo " make wasm - Build WASM for web (--target web, crypto-zk) @echo " make wasm-node - Build WASM for Node.js" @echo " make wasm-all - Build all WASM targets" @echo " make fmt - Format code" From 3c92beb91349fc79aebfc120d09ea8e59cd3be3b Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:47:21 -0300 Subject: [PATCH 8/9] feat(crypto): implement default constructor for ZkCryptoProvider and optimize hasher usage --- src/infrastructure/crypto.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/infrastructure/crypto.rs b/src/infrastructure/crypto.rs index a04f4c3..9de7426 100644 --- a/src/infrastructure/crypto.rs +++ b/src/infrastructure/crypto.rs @@ -30,6 +30,13 @@ use alloc::string::String; #[cfg(any(feature = "crypto-zk", feature = "crypto"))] pub struct ZkCryptoProvider; +#[cfg(any(feature = "crypto-zk", feature = "crypto"))] +impl Default for ZkCryptoProvider { + fn default() -> Self { + Self::new() + } +} + #[cfg(any(feature = "crypto-zk", feature = "crypto"))] impl ZkCryptoProvider { pub fn new() -> Self { @@ -62,7 +69,7 @@ impl ZkCryptoProvider { } pub fn get_note_commitment(&self, note: &Note) -> [u8; 32] { - let hasher = LightPoseidonHasher::default(); + let hasher = LightPoseidonHasher; let commitment = note.commitment(hasher); Self::field_to_bytes(commitment.inner()) } @@ -106,7 +113,7 @@ impl ZkCryptoProvider { let commitment_field = Self::bytes_to_field(commitment)?; let spending_key_field = Self::bytes_to_field(spending_key)?; - let hasher = LightPoseidonHasher::default(); + let hasher = LightPoseidonHasher; let nullifier = hasher.hash_2([commitment_field, spending_key_field]); Ok(Self::field_to_bytes(nullifier)) @@ -116,7 +123,7 @@ impl ZkCryptoProvider { let left_field = Self::bytes_to_field(left)?; let right_field = Self::bytes_to_field(right)?; - let hasher = LightPoseidonHasher::default(); + let hasher = LightPoseidonHasher; let result = hasher.hash_2([left_field, right_field]); Ok(Self::field_to_bytes(result)) @@ -128,7 +135,7 @@ impl ZkCryptoProvider { field_inputs[i] = Self::bytes_to_field(*input)?; } - let hasher = LightPoseidonHasher::default(); + let hasher = LightPoseidonHasher; let result = hasher.hash_4(field_inputs); Ok(Self::field_to_bytes(result)) @@ -193,7 +200,7 @@ impl EcdsaSigner { let mut compressed = [0u8; 33]; compressed[0] = prefix; compressed[1..].copy_from_slice(&pubkey_uncompressed[1..33]); - let account_id: [u8; 32] = Blake2b::::digest(&compressed).into(); + let account_id: [u8; 32] = Blake2b::::digest(compressed).into(); let address = Address(account_id); Ok(EcdsaSigner { From 2d6ab74b4b0002c48cc9f186a68b2252fd0ea136 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Mon, 23 Feb 2026 16:47:25 -0300 Subject: [PATCH 9/9] feat(signing): add clippy allowance for too many arguments in sign_and_build_unshield --- src/presentation/api/signing.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presentation/api/signing.rs b/src/presentation/api/signing.rs index b00d947..c33a3ae 100644 --- a/src/presentation/api/signing.rs +++ b/src/presentation/api/signing.rs @@ -56,6 +56,7 @@ impl SigningApi { } /// Signs and builds complete Unshield transaction. + #[allow(clippy::too_many_arguments)] pub fn sign_and_build_unshield( nullifier: [u8; 32], amount: u128,