diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index cdf626f..44da47b 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -25,7 +25,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aleo" -version = "0.3.1" +version = "0.4.0" dependencies = [ "anyhow", "hex", @@ -1289,9 +1289,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.14.0-rc.15" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ce7f1aa9a24c53c6572d8017c8c1ceb5d44c6071ff68c9912860fa4d262101" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" dependencies = [ "cpubits", "ecdsa", @@ -2310,8 +2310,8 @@ dependencies = [ [[package]] name = "snarkvm" -version = "4.8.1" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "rand", @@ -2327,8 +2327,8 @@ dependencies = [ [[package]] name = "snarkvm-algorithms" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -2354,8 +2354,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2368,8 +2368,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-account" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-network", "snarkvm-circuit-types", @@ -2378,8 +2378,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-algorithms" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-types", "snarkvm-console-algorithms", @@ -2388,8 +2388,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-collections" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-types", @@ -2398,8 +2398,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "indexmap", @@ -2418,13 +2418,13 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment-witness" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" [[package]] name = "snarkvm-circuit-network" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-collections", @@ -2434,8 +2434,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-program" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2448,8 +2448,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-address", @@ -2463,8 +2463,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-address" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2476,8 +2476,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-boolean" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-console-types-boolean", @@ -2485,8 +2485,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-field" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2495,8 +2495,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-group" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2507,8 +2507,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-integers" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2519,8 +2519,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-scalar" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2530,8 +2530,8 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-string" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2542,8 +2542,8 @@ dependencies = [ [[package]] name = "snarkvm-console" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-account", "snarkvm-console-algorithms", @@ -2555,8 +2555,8 @@ dependencies = [ [[package]] name = "snarkvm-console-account" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "bs58", "snarkvm-console-network", @@ -2566,12 +2566,13 @@ dependencies = [ [[package]] name = "snarkvm-console-algorithms" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "blake2s_simd", "hex", "k256", + "rayon", "serde", "smallvec", "snarkvm-console-types", @@ -2582,8 +2583,8 @@ dependencies = [ [[package]] name = "snarkvm-console-collections" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "parking_lot", @@ -2595,8 +2596,8 @@ dependencies = [ [[package]] name = "snarkvm-console-network" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "enum-iterator", @@ -2615,8 +2616,8 @@ dependencies = [ [[package]] name = "snarkvm-console-network-environment" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "bech32", @@ -2633,8 +2634,8 @@ dependencies = [ [[package]] name = "snarkvm-console-program" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "enum-iterator", "enum_index", @@ -2654,8 +2655,8 @@ dependencies = [ [[package]] name = "snarkvm-console-types" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-address", @@ -2669,8 +2670,8 @@ dependencies = [ [[package]] name = "snarkvm-console-types-address" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2680,16 +2681,16 @@ dependencies = [ [[package]] name = "snarkvm-console-types-boolean" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", ] [[package]] name = "snarkvm-console-types-field" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2698,8 +2699,8 @@ dependencies = [ [[package]] name = "snarkvm-console-types-group" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2709,8 +2710,8 @@ dependencies = [ [[package]] name = "snarkvm-console-types-integers" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2720,8 +2721,8 @@ dependencies = [ [[package]] name = "snarkvm-console-types-scalar" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2731,8 +2732,8 @@ dependencies = [ [[package]] name = "snarkvm-console-types-string" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2742,8 +2743,8 @@ dependencies = [ [[package]] name = "snarkvm-curves" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "rand", "rustc_version", @@ -2755,8 +2756,8 @@ dependencies = [ [[package]] name = "snarkvm-fields" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -2772,8 +2773,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -2802,8 +2803,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-authority" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "rand", @@ -2814,8 +2815,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-block" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "indexmap", @@ -2838,8 +2839,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-committee" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "indexmap", "rayon", @@ -2850,8 +2851,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-ledger-narwhal-batch-certificate", "snarkvm-ledger-narwhal-batch-header", @@ -2863,8 +2864,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-certificate" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "indexmap", "rayon", @@ -2876,8 +2877,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-header" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "indexmap", "rayon", @@ -2888,8 +2889,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-data" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "bytes", "serde_json", @@ -2899,8 +2900,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-subdag" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "indexmap", "rayon", @@ -2914,8 +2915,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "bytes", "serde_json", @@ -2927,8 +2928,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission-id" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "snarkvm-console", "snarkvm-ledger-puzzle", @@ -2936,8 +2937,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -2956,8 +2957,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle-epoch" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -2978,8 +2979,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-query" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "async-trait", @@ -2995,8 +2996,8 @@ dependencies = [ [[package]] name = "snarkvm-ledger-store" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std-storage", "anyhow", @@ -3020,8 +3021,8 @@ dependencies = [ [[package]] name = "snarkvm-parameters" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -3042,8 +3043,8 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -3077,8 +3078,8 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-error" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "anyhow", "snarkvm-circuit-environment", @@ -3089,8 +3090,8 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-process" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "colored", @@ -3115,8 +3116,8 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-program" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "enum-iterator", "indexmap", @@ -3136,8 +3137,8 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-snark" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "bincode", "serde_json", @@ -3149,8 +3150,8 @@ dependencies = [ [[package]] name = "snarkvm-utilities" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "aleo-std", "anyhow", @@ -3172,8 +3173,8 @@ dependencies = [ [[package]] name = "snarkvm-utilities-derives" -version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +version = "4.9.0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.9.0#8902106ccc810d784ae9edb90ebfde17f94ef41a" dependencies = [ "proc-macro2", "quote 1.0.46", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 7444d41..948895c 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -29,7 +29,7 @@ serde = "1" serde_json = "1" sha2 = "0.10" -snarkvm = { git = "https://github.com/ProvableHQ/snarkVM.git", tag = "v4.8.1", default-features = false, features = [ +snarkvm = { git = "https://github.com/ProvableHQ/snarkVM.git", tag = "v4.9.0", default-features = false, features = [ "console", "circuit", "synthesizer", "ledger", "utilities", "algorithms", "parameters", ] } diff --git a/sdk/python/aleo/__init__.py b/sdk/python/aleo/__init__.py index df109e5..45082d1 100644 --- a/sdk/python/aleo/__init__.py +++ b/sdk/python/aleo/__init__.py @@ -12,6 +12,7 @@ pass from .encryptor import * +from .merkle import MerkleExclusionProof as MerkleExclusionProof from .network_client import AleoNetworkClient as AleoNetworkClient from .async_network_client import AsyncAleoNetworkClient as AsyncAleoNetworkClient from ._client_common import AleoNetworkError as AleoNetworkError diff --git a/sdk/python/aleo/async_network_client.py b/sdk/python/aleo/async_network_client.py index 2ba8d2d..66f6608 100644 --- a/sdk/python/aleo/async_network_client.py +++ b/sdk/python/aleo/async_network_client.py @@ -739,6 +739,45 @@ async def get_program_mapping_value( "getProgramMappingValue", ) + async def get_freeze_list(self, program_id: str) -> list[int]: + """Read a compliance program's freeze-list Merkle tree. + + Programs following the Sealance architecture publish their freeze list + as a sorted Merkle tree and require callers to prove non-inclusion in + it. Pair this with :class:`~aleo.MerkleExclusionProof` to turn the tree + into the proof a transition expects. + + Args: + program_id: The freeze-list program, e.g. + ``"shield_swap_freezelist.aleo"``. Each compliance program + keeps its own list, so this is a parameter rather than a + constant. + + Returns: + Every node of the tree in tree order — leaves first, then each layer + above, with the Merkle root last. An empty list reads back as the + two-leaf zero tree. + + Raises: + AleoNetworkError: If the program does not exist or serves no list. + ValueError: If the response is not an array of field values. + """ + payload = await self._get( + f"/programs/{program_id}/compliance/freeze-list", + "getFreezeList", + ) + if not isinstance(payload, list): + raise ValueError( + f"{program_id} returned a {type(payload).__name__} rather than " + f"a freeze list array" + ) + try: + return [int(str(node).strip().removesuffix("field")) for node in payload] + except ValueError: + raise ValueError( + f"{program_id} freeze list holds a non-numeric node" + ) from None + async def get_public_balance(self, address: str) -> int: """Read an address's public ``credits.aleo`` balance. diff --git a/sdk/python/aleo/merkle.py b/sdk/python/aleo/merkle.py new file mode 100644 index 0000000..189e839 --- /dev/null +++ b/sdk/python/aleo/merkle.py @@ -0,0 +1,344 @@ +# Copyright (C) 2019-2026 Provable Inc. +# This file is part of the Aleo SDK library. + +# The Aleo SDK library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# The Aleo SDK library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with the Aleo SDK library. If not, see . + +"""Merkle exclusion (non-inclusion) proofs over sorted address trees. + +Programs following the Sealance compliance architecture keep a freeze list as a +sorted Merkle tree of addresses and require callers to prove their address is +*absent* from it. This module builds those proofs client-side. + +It is program-agnostic: it knows about sorted address trees and nothing about +any particular freeze list, so it serves ``shield_swap_freezelist.aleo``, the +compliance stablecoins, and anything else built the same way. + +Ported from the wasm SDK's ``SealanceMerkleTree`` +(``ProvableHQ/sdk@mainnet:sdk/src/integrations/sealance/merkle-tree.ts``). +""" +from __future__ import annotations + +from typing import Any, NamedTuple, Sequence + +from ._client_common import DEFAULT_NETWORK + +__all__ = ["MerkleExclusionProof", "SiblingPath"] + + +class SiblingPath(NamedTuple): + """One authentication path, as the verifier's ``MerkleProof`` struct. + + ``siblings[0]`` is the leaf itself and ``siblings[1]`` its leaf-layer + sibling; the rest are the sibling at each level above, zero-padded to fill + the struct's fixed-size array. + """ + + siblings: list[int] + leaf_index: int + +#: The all-zero address, which the tree carries as padding rather than as a +#: member. Verifiers compare against it, so it is never a real leaf. +ZERO_ADDRESS = "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc" + +#: Domain separators. Leaf-layer pairs hash under ``1field``, every layer above +#: under ``0field``, which is what keeps a leaf from being replayed as a node. +_LEAF_PREFIX = "1field" +_NODE_PREFIX = "0field" + + +class MerkleExclusionProof: + """Builds Merkle non-inclusion proofs against a sorted address tree. + + Args: + max_depth: Maximum tree depth, matching the verifying program's + ``MAX_MERKLE_TREE_DEPTH``. This is the single knob the rest of the + shape derives from: the tree holds up to ``2 ** max_depth`` leaves + and a proof carries ``max_depth + 1`` siblings (one leaf slot plus + one per level). Defaults to 15, the shield_swap value. + network: Which extension module supplies ``Poseidon4`` and ``Address``. + """ + + def __init__(self, *, max_depth: int = 15, + network: str = DEFAULT_NETWORK) -> None: + if max_depth < 1: + raise ValueError(f"max_depth must be at least 1, got {max_depth}") + self.max_depth = max_depth + self._network = network + # Built on first use and reused: a full-depth tree is 65_535 hashes, + # and Poseidon setup is not free. + self._hasher: Any = None + + def __repr__(self) -> str: + return (f"MerkleExclusionProof(max_depth={self.max_depth}, " + f"network={self._network!r})") + + def _net(self) -> Any: + """The network extension module supplying the hash and address types.""" + try: + if self._network == "testnet": + from . import testnet as _mod # type: ignore[attr-defined] + else: + from . import mainnet as _mod # type: ignore[attr-defined] + except ImportError: + raise ImportError( + f"aleo {self._network} module not available" + ) from None + return _mod + + # ── Tree construction ──────────────────────────────────────────────── + + def leaves_from_addresses(self, addresses: Sequence[str], *, + max_depth: int | None = None) -> list[str]: + """Addresses as sorted, zero-padded leaf literals. + + Zero addresses are dropped, the rest are converted to fields and sorted + ascending, then ``0field`` padding is *prepended* to reach a power of + two. Prepending is what keeps the greatest real address at the right + boundary, which is the invariant the above-last case relies on. + + Args: + addresses: The member addresses, in any order. + max_depth: Overrides the instance's depth for this call. + + Returns: + ``2 ** k`` field literals, ordered padding-first then ascending. + + Raises: + ValueError: If there are more addresses than ``2 ** max_depth``. + """ + depth = self.max_depth if max_depth is None else max_depth + capacity = 2 ** depth + + members = [a for a in addresses if a != ZERO_ADDRESS] + if len(members) > capacity: + raise ValueError( + f"freeze list holds {len(members)} addresses, but a depth-" + f"{depth} tree caps at {capacity}" + ) + + num_leaves = 2 if len(members) <= 1 else 1 << (len(members) - 1).bit_length() + + fields = sorted(self.address_to_field(a) for a in members) + padding = ["0field"] * (num_leaves - len(fields)) + return padding + [f"{f}field" for f in fields] + + def build_tree(self, leaves: Sequence[str]) -> list[int]: + """Hash *leaves* bottom-up into a flat tree array. + + Args: + leaves: An even number of field literals, as + :meth:`leaves_from_addresses` returns. + + Returns: + Every node as an int, level by level: leaves first, then each layer + above, ending with the root. + + Raises: + ValueError: If *leaves* is empty or has an odd length. + """ + if not leaves: + raise ValueError("leaves cannot be empty") + if len(leaves) % 2 != 0: + raise ValueError( + f"leaves must have an even length, got {len(leaves)}") + + current = list(leaves) + tree = list(current) + while len(current) > 1: + # The leaf layer is the only one whose width equals the input's. + prefix = _LEAF_PREFIX if len(current) == len(leaves) else _NODE_PREFIX + current = [ + self._hash_pair(prefix, current[i], current[i + 1]) + for i in range(0, len(current), 2) + ] + tree.extend(current) + return [int(node.removesuffix("field")) for node in tree] + + def tree_from_nodes(self, nodes: Sequence[str]) -> list[int]: + """A pre-built tree from its serialized nodes. + + Freeze-list services publish the whole tree — leaves, then each layer + above, root last — as decimal strings, so the common path needs no + hashing at all. + + Args: + nodes: Every node as a decimal string, in tree order. A ``field`` + suffix is tolerated. + + Returns: + The same nodes as ints, ready for :meth:`exclusion_proof`. + + Raises: + ValueError: If *nodes* is empty, is not a whole tree + (``2 * leaves - 1`` nodes), or holds a non-numeric entry. + """ + if not nodes: + raise ValueError("a tree needs at least one node") + try: + tree = [int(str(node).strip().removesuffix("field")) for node in nodes] + except ValueError as exc: + raise ValueError(f"tree holds a non-numeric node: {exc}") from None + + # A complete binary tree over 2**k leaves has 2**(k+1) - 1 nodes. + if (len(tree) + 1) & len(tree): + raise ValueError( + f"{len(tree)} nodes is not a complete tree — expected " + f"2 * leaves - 1" + ) + return tree + + def root(self, tree: Sequence[int]) -> str: + """The tree's root as a field literal.""" + if not tree: + raise ValueError("tree cannot be empty") + return f"{tree[-1]}field" + + # ── Proofs ─────────────────────────────────────────────────────────── + + def leaf_indices(self, tree: Sequence[int], address: str) -> tuple[int, int]: + """The two leaves whose paths together prove *address* is absent. + + Args: + tree: A flat tree as :meth:`build_tree` returns. + address: The address to exclude. + + Returns: + ``(left, right)``. Normally these bracket the address as adjacent + leaves. They collapse to a single index at the boundaries: both + ``0`` when the address sorts below every leaf, and both the last + index when it sorts above every leaf — the two special cases the + verifier checks separately. + + Raises: + ValueError: If *address* is a member. A member cannot be proven + absent, and returning indices anyway would produce a proof that + fails only after the caller has paid to prove and broadcast it. + """ + num_leaves = (len(tree) + 1) // 2 + leaves = list(tree[:num_leaves]) + value = self.address_to_field(address) + + if value in leaves: + raise ValueError( + f"{address} is on the list (leaf {leaves.index(value)}), so it " + f"cannot be proven excluded" + ) + + right = next((i for i, leaf in enumerate(leaves) if value <= leaf), -1) + if right == -1: + return num_leaves - 1, num_leaves - 1 + if right == 0: + return 0, 0 + return right - 1, right + + def sibling_path(self, tree: Sequence[int], leaf_index: int, *, + max_depth: int | None = None) -> SiblingPath: + """The authentication path for *leaf_index*. + + Args: + tree: A flat tree as :meth:`build_tree` returns. + leaf_index: Which leaf to authenticate. + max_depth: Overrides the instance's depth for this call. + + Returns: + A :class:`SiblingPath` holding exactly ``max_depth + 1`` siblings. + The array is padded to its full width rather than to the tree's own + depth: the verifier reads a fixed-size array and treats the trailing + zeros as "no more levels", so a short path fails to typecheck and a + long one cannot be represented. + + Raises: + IndexError: If *leaf_index* is outside the tree's leaf layer. + ValueError: If the tree is deeper than ``max_depth`` allows. + """ + depth = self.max_depth if max_depth is None else max_depth + width = depth + 1 + + num_leaves = (len(tree) + 1) // 2 + if not 0 <= leaf_index < num_leaves: + raise IndexError( + f"leaf_index {leaf_index} is outside a tree of {num_leaves} " + f"leaves" + ) + + siblings = [tree[leaf_index]] + index = leaf_index + parent_index = num_leaves + level = 1 + while parent_index < len(tree): + sibling = index + 1 if index % 2 == 0 else index - 1 + siblings.append(tree[sibling]) + index = parent_index + leaf_index // (2 ** level) + parent_index += num_leaves // (2 ** level) + level += 1 + + if len(siblings) > width: + raise ValueError( + f"tree of {num_leaves} leaves needs {len(siblings)} proof " + f"slots, but max_depth={depth} allows {width}" + ) + siblings.extend([0] * (width - len(siblings))) + return SiblingPath(siblings, leaf_index) + + def format_proof(self, paths: Sequence[SiblingPath]) -> str: + """Authentication paths as the Aleo array-of-struct literal.""" + structs = ", ".join( + "{siblings: [" + ", ".join(f"{s}field" for s in path.siblings) + + f"], leaf_index: {path.leaf_index}u32}}" + for path in paths + ) + return f"[{structs}]" + + def exclusion_proof(self, tree: Sequence[int], address: str, *, + max_depth: int | None = None) -> str: + """A ready-to-pass ``[MerkleProof; 2]`` literal proving *address* is absent. + + Args: + tree: A flat tree as :meth:`build_tree` or :meth:`tree_from_nodes` + returns. + address: The address to prove absent. + max_depth: Overrides the instance's depth for this call. + + Returns: + The literal to pass wherever the program takes ``[MerkleProof; 2]``. + + Raises: + ValueError: If *address* is on the list. + """ + left, right = self.leaf_indices(tree, address) + return self.format_proof([ + self.sibling_path(tree, left, max_depth=max_depth), + self.sibling_path(tree, right, max_depth=max_depth), + ]) + + # ── Primitives ─────────────────────────────────────────────────────── + + def address_to_field(self, address: str) -> int: + """An Aleo address as its field element, matching Leo's ``as field``.""" + net = self._net() + return int(str(net.Address.from_string(address).to_field()) + .removesuffix("field")) + + def _hash_pair(self, prefix: str, left: str, right: str) -> str: + """``Poseidon4`` over ``[prefix, left, right]`` as a field literal. + + The operands go through ``Plaintext`` so they hash as a 3-element array, + which is what the Leo side does. Hashing a bare list of fields, or + going through ``to_fields_raw``, gives a different and wrong answer. + """ + net = self._net() + if self._hasher is None: + self._hasher = net.Poseidon4() + plaintext = net.Plaintext.from_string(f"[{prefix},{left},{right}]") + return str(self._hasher.hash(plaintext.to_fields())) diff --git a/sdk/python/aleo/network_client.py b/sdk/python/aleo/network_client.py index ed8d388..4aabfe3 100644 --- a/sdk/python/aleo/network_client.py +++ b/sdk/python/aleo/network_client.py @@ -740,6 +740,45 @@ def get_program_mapping_plaintext( ) return Plaintext.from_string(json.loads(raw)) + def get_freeze_list(self, program_id: str) -> list[int]: + """Read a compliance program's freeze-list Merkle tree. + + Programs following the Sealance architecture publish their freeze list + as a sorted Merkle tree and require callers to prove non-inclusion in + it. Pair this with :class:`~aleo.MerkleExclusionProof` to turn the tree + into the proof a transition expects. + + Args: + program_id: The freeze-list program, e.g. + ``"shield_swap_freezelist.aleo"``. Each compliance program + keeps its own list, so this is a parameter rather than a + constant. + + Returns: + Every node of the tree in tree order — leaves first, then each layer + above, with the Merkle root last. An empty list reads back as the + two-leaf zero tree. + + Raises: + AleoNetworkError: If the program does not exist or serves no list. + ValueError: If the response is not an array of field values. + """ + payload = self._get( + f"/programs/{program_id}/compliance/freeze-list", + "getFreezeList", + ) + if not isinstance(payload, list): + raise ValueError( + f"{program_id} returned a {type(payload).__name__} rather than " + f"a freeze list array" + ) + try: + return [int(str(node).strip().removesuffix("field")) for node in payload] + except ValueError: + raise ValueError( + f"{program_id} freeze list holds a non-numeric node" + ) from None + def get_public_balance(self, address: str) -> int: """Read an address's public ``credits.aleo`` balance. diff --git a/sdk/python/tests/test_merkle.py b/sdk/python/tests/test_merkle.py new file mode 100644 index 0000000..19c278b --- /dev/null +++ b/sdk/python/tests/test_merkle.py @@ -0,0 +1,318 @@ +# Copyright (C) 2019-2026 Provable Inc. +# This file is part of the Aleo SDK library. + +# The Aleo SDK library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# The Aleo SDK library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with the Aleo SDK library. If not, see . + +"""Tests for :class:`aleo.MerkleExclusionProof`. + +The known answers come from deployed state rather than from the TypeScript +reference, so the port is pinned to what the chain actually verifies: + +- ``EMPTY_TREE_ROOT`` is ``shield_swap_freezelist.aleo``'s initial + ``freeze_list_root[1u8]``, read from testnet 2026-08-06. +- The address/field pairs are ``SealanceMerkleTree``'s own docstring examples + (``ProvableHQ/sdk@mainnet:sdk/src/integrations/sealance/merkle-tree.ts``). +""" + +import re + +import pytest + +from aleo import MerkleExclusionProof +from aleo.mainnet import Address, Plaintext, Poseidon4 + + +#: ``Poseidon4([1field, 0field, 0field])`` — the root of a two-leaf empty tree. +EMPTY_TREE_ROOT = ( + "3642222252059314292809609689035560016959342421640560347114299934615987159853field" +) + +ADDR_A = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" +ADDR_B = "aleo1s3ws5tra87fjycnjrwsjcrnw2qxr8jfqqdugnf0xzqqw29q9m5pqem2u4t" +ZERO_ADDRESS = "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc" + +#: Nine freeze-list members in ascending field order. Taking the first ``n`` +#: gives member counts either side of a power of two, which is what exercises +#: the zero padding. +MEMBERS = [ + "aleo10p6fdm4054p50e7ykx5f4ec5hy0gt5pwnm0xyj8rr5vx6pva5cqsyx5k5y", + "aleo13smp4u3ctasl5xnsrg9slcagsk384kvsk3enu9z0n3qsjcwjgcps4623cu", + "aleo1z0c4vn2erd0908020slz5muqm459c5kkyle2xy4jc3n5xh2w6uzq508wr7", + "aleo1zh8kzhr0eydshme7gjdj9ynf47pv6xg3al8mylx32yl4770t3vrqld2y2a", + "aleo15ls2v3ga6cxx34ddeqsm5f0ksrvwjaj60e8pztdyxu2fl6lpfyyqu93feh", + "aleo1wg0jwg4x9a2fmwhu2dkwdv2jd9j4vzmzp5gldjeyyzkmhj5lyy9q95m824", + "aleo1l6qyu9jl34ged8he4sfglcfuxjpupyyulvkjt2qa2qwj6s93ag9snnc929", + "aleo1fvx6rtmd6swdgwllgua6ygjjvej73nqj5uakzj9fe2sal8xe4cxsgr7lyq", + "aleo1fr5nxtkyflk83zq3jxr3w6tsa7akaj0eslvy83suu6pwut03958sjwvkdx", +] +#: Falls between ``MEMBERS[1]`` and ``MEMBERS[2]`` — the bracketed case. +TARGET_INSIDE = "aleo178tq4f3qpcggwgt4l0xdashky3k2tun2lfm9lu6dt0y7h309fsps4hszf8" +#: Below every member — the below-first case. +TARGET_BELOW = "aleo1x9m7n0vx0rd8am8qdr2f5m8u2e2zwzmj53yeh5jszl57yp5lqqqqjm8wh3" +#: Above every member — the above-last case. +TARGET_ABOVE = "aleo1c9pnkwzja5m5dj93dg80gjyale8ep0n8zgrwl8cpt0r0cu874gfqyzrcgf" + + +@pytest.fixture +def merkle() -> MerkleExclusionProof: + return MerkleExclusionProof() + + +# ── A independent reimplementation of the on-chain verifier ────────────────── +# +# Transcribed from ``verify_merkle_non_inclusion`` and +# ``calculate_merkle_root_and_depth`` in +# ``ProvableHQ/amm-v3@development:src/main.leo`` (b66d4f2), which matches the +# bytecode deployed as ``shield_swap.aleo``. It hashes via its own Poseidon4 +# calls rather than through the class under test, so it can disagree with the +# implementation — which is the point. + + +def _hash3(prefix: str, left: int, right: int) -> int: + plaintext = Plaintext.from_string(f"[{prefix},{left}field,{right}field]") + return int(str(Poseidon4().hash(plaintext.to_fields())).removesuffix("field")) + + +def parse_proof_literal(literal: str) -> list[tuple[list[int], int]]: + """``[{siblings: [...], leaf_index: Nu32}, ...]`` back into Python.""" + parsed = [] + for block in re.findall(r"\{[^{}]*\}", literal): + siblings = [int(s) for s in re.findall(r"(\d+)field", block)] + leaf_index = int(re.search(r"leaf_index:\s*(\d+)u32", block).group(1)) + parsed.append((siblings, leaf_index)) + return parsed + + +def leo_root_and_depth(siblings: list[int], leaf_index: int, + max_depth: int = 15) -> tuple[int, int]: + root = _hash3("1field", *( + (siblings[0], siblings[1]) if leaf_index % 2 == 0 + else (siblings[1], siblings[0]))) + for i in range(2, max_depth + 1): + if siblings[i] == 0: + return root, i - 1 + pair = ((root, siblings[i]) if (leaf_index // 2 ** (i - 1)) % 2 == 0 + else (siblings[i], root)) + root = _hash3("0field", *pair) + return root, max_depth + + +def leo_verify_non_inclusion(literal: str, address: str, + max_depth: int = 15) -> int: + """Return the proven root, or raise ``AssertionError`` as the contract aborts.""" + proofs = parse_proof_literal(literal) + assert len(proofs) == 2, "the verifier takes exactly two paths" + (sib0, idx0), (sib1, idx1) = proofs + + root0, depth0 = leo_root_and_depth(sib0, idx0, max_depth) + root1, depth1 = leo_root_and_depth(sib1, idx1, max_depth) + assert root0 == root1, "the two paths must share a root" + assert depth0 == depth1, "the two paths must share a depth" + + value = int(str(Address.from_string(address).to_field()).removesuffix("field")) + last_leaf_index = 2 ** depth0 - 1 + if idx0 == idx1: + if idx0 == 0: + assert value < sib0[0], "below-first: value must precede leaf 0" + else: + assert idx0 == last_leaf_index, "above-last: must be the final leaf" + assert value > sib0[0], "above-last: value must follow the final leaf" + else: + assert value > sib0[0], "bracketed: value must follow the left leaf" + assert value < sib1[0], "bracketed: value must precede the right leaf" + assert idx1 <= last_leaf_index, "bracketed: right leaf out of range" + assert idx0 + 1 == idx1, "bracketed: leaves must be adjacent" + return root0 + + +def synthetic_tree(merkle: MerkleExclusionProof, num_leaves: int) -> list[int]: + """A tree of *num_leaves* whose leaves are the fields ``10, 20, 30, ...``. + + Built directly from field literals rather than addresses so a test can + place a target value precisely between two members. + """ + leaves = [f"{(i + 1) * 10}field" for i in range(num_leaves)] + return merkle.build_tree(leaves) + + +def test_empty_freezelist_root_matches_the_deployed_root(merkle): + """An empty address list must reproduce the freezelist's on-chain root.""" + tree = merkle.build_tree(merkle.leaves_from_addresses([])) + + assert merkle.root(tree) == EMPTY_TREE_ROOT + + +@pytest.mark.parametrize("depth", range(1, 16)) +def test_sibling_path_always_fills_the_proof_array(merkle, depth): + """Every proof carries ``max_depth + 1`` siblings, whatever the tree depth. + + The reference implementation pads to ``depth`` instead, yielding 15 slots + for every tree below the maximum and 16 only at depth 15. The verifying + struct is ``[field; 16]``, so a short path is rejected outright. + + The path's width depends only on the tree's size, so the sweep uses stand-in + node values; :func:`test_sibling_path_of_a_hashed_tree_fills_the_array` + covers a genuinely hashed tree. + """ + num_leaves = 2 ** depth + tree = list(range(1, 2 * num_leaves)) + + path = merkle.sibling_path(tree, 0) + + assert len(path.siblings) == merkle.max_depth + 1 + + +def test_sibling_path_of_a_hashed_tree_fills_the_array(merkle): + """The width also holds for a real Poseidon-hashed tree.""" + tree = synthetic_tree(merkle, 8) + + path = merkle.sibling_path(tree, 3) + + assert len(path.siblings) == 16 + + +#: Eight members fill a tree exactly, so there is no padding and leaf 0 is a +#: real address. That is the only shape in which the verifier's below-first +#: case can arise. +UNPADDED = MEMBERS[:8] + + +def test_leaf_indices_bracket_an_absent_address(merkle): + """An address inside the range brackets the two leaves either side of it.""" + tree = merkle.build_tree(merkle.leaves_from_addresses(UNPADDED)) + + assert merkle.leaf_indices(tree, TARGET_INSIDE) == (1, 2) + + +def test_leaf_indices_below_every_member_collapse_to_the_first_leaf(merkle): + """Below an unpadded list, both paths point at leaf 0.""" + tree = merkle.build_tree(merkle.leaves_from_addresses(UNPADDED)) + + assert merkle.leaf_indices(tree, TARGET_BELOW) == (0, 0) + + +def test_leaf_indices_above_every_member_collapse_to_the_last_leaf(merkle): + """Above the whole list, both paths point at the final leaf.""" + tree = merkle.build_tree(merkle.leaves_from_addresses(UNPADDED)) + + assert merkle.leaf_indices(tree, TARGET_ABOVE) == (7, 7) + + +def test_leaf_indices_bracket_against_padding_below_the_first_member(merkle): + """With padding, a below-everything address brackets the last zero leaf. + + Nine members pad to sixteen leaves, so leaf 6 is ``0field`` and leaf 7 is + the smallest real address. Every address sorts above zero, so this is the + ordinary bracketed case rather than the below-first one. + """ + tree = merkle.build_tree(merkle.leaves_from_addresses(MEMBERS)) + + assert merkle.leaf_indices(tree, TARGET_BELOW) == (6, 7) + + +def test_leaf_indices_reject_an_address_that_is_on_the_list(merkle): + """A member cannot be proven absent, so ask for it and get an error. + + The reference brackets with ``<=``, which hands back indices whose proof + fails the verifier's strict inequality — but only after the caller has paid + to prove and broadcast it. + """ + tree = merkle.build_tree(merkle.leaves_from_addresses(MEMBERS)) + + with pytest.raises(ValueError, match="on the list"): + merkle.leaf_indices(tree, MEMBERS[2]) + + +@pytest.mark.parametrize("target,case", [ + (TARGET_INSIDE, "bracketed"), + (TARGET_BELOW, "below-first"), + (TARGET_ABOVE, "above-last"), +]) +def test_exclusion_proof_satisfies_the_contract_verifier(merkle, target, case): + """Every generated proof is accepted by the transcribed on-chain verifier. + + This is what pins the port to the contract rather than to the TypeScript: + the oracle hashes independently, so a wrong ordering, index bit, or domain + separator shows up here. + """ + tree = merkle.build_tree(merkle.leaves_from_addresses(UNPADDED)) + + literal = merkle.exclusion_proof(tree, target) + + assert leo_verify_non_inclusion(literal, target) == tree[-1], case + + +@pytest.mark.parametrize("count", [0, 1, 2, 3, 5, 7, 9]) +@pytest.mark.parametrize("target", [TARGET_INSIDE, TARGET_BELOW, TARGET_ABOVE]) +def test_exclusion_proof_survives_zero_padded_trees(merkle, count, target): + """Member counts that are not powers of two pad with leading ``0field``.""" + tree = merkle.build_tree(merkle.leaves_from_addresses(MEMBERS[:count])) + + literal = merkle.exclusion_proof(tree, target) + + assert leo_verify_non_inclusion(literal, target) == tree[-1] + + +def test_exclusion_proof_literal_matches_the_struct(merkle): + """The literal is two ``MerkleProof`` structs with full 16-slot arrays.""" + tree = merkle.build_tree(merkle.leaves_from_addresses(MEMBERS)) + + literal = merkle.exclusion_proof(tree, TARGET_INSIDE) + + proofs = parse_proof_literal(literal) + assert len(proofs) == 2 + assert all(len(siblings) == 16 for siblings, _ in proofs) + assert literal.startswith("[{") and literal.endswith("}]") + + +def test_leaves_capacity_matches_the_depth(merkle): + """A depth-``d`` tree holds ``2 ** d`` addresses, not ``2 ** (d - 1)``.""" + assert len(MerkleExclusionProof(max_depth=2).leaves_from_addresses(MEMBERS[:4])) == 4 + with pytest.raises(ValueError, match="caps at 2"): + MerkleExclusionProof(max_depth=1).leaves_from_addresses(MEMBERS[:4]) + + +def test_tree_from_nodes_reads_the_freeze_list_endpoint_shape(merkle): + """The endpoint serves the whole tree as decimal strings, root last.""" + tree = merkle.tree_from_nodes( + ["0", "0", EMPTY_TREE_ROOT.removesuffix("field")]) + + assert merkle.root(tree) == EMPTY_TREE_ROOT + + +def test_tree_from_nodes_round_trips_a_built_tree(merkle): + """A served tree proves exactly like one built locally from addresses.""" + built = merkle.build_tree(merkle.leaves_from_addresses(MEMBERS)) + + served = merkle.tree_from_nodes([str(node) for node in built]) + + assert served == built + assert leo_verify_non_inclusion( + merkle.exclusion_proof(served, TARGET_INSIDE), TARGET_INSIDE) == built[-1] + + +def test_zero_addresses_are_not_members(merkle): + """The zero address is padding, so it never becomes a leaf of its own.""" + with_zero = merkle.leaves_from_addresses([ZERO_ADDRESS, *MEMBERS[:3]]) + + assert with_zero == merkle.leaves_from_addresses(MEMBERS[:3]) + + +def test_sibling_path_width_follows_a_custom_max_depth(merkle): + """``max_depth`` is the single knob: the array is ``max_depth + 1`` wide.""" + shallow = MerkleExclusionProof(max_depth=10) + tree = synthetic_tree(shallow, 8) + + assert len(shallow.sibling_path(tree, 0).siblings) == 11 + assert len(merkle.sibling_path(tree, 0, max_depth=4).siblings) == 5 diff --git a/sdk/python/tests/test_network_client.py b/sdk/python/tests/test_network_client.py index 5c25cb8..8123f16 100644 --- a/sdk/python/tests/test_network_client.py +++ b/sdk/python/tests/test_network_client.py @@ -875,3 +875,52 @@ def test_set_prover_uri() -> None: c = make_client() c.set_prover_uri("https://prover.example.com") assert c._prover_uri == f"https://prover.example.com/{NET}" + + +# --------------------------------------------------------------------------- +# Freeze list (compliance) +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_get_freeze_list_url_takes_the_program() -> None: + """The program is a parameter — one endpoint serves every freeze list.""" + resp_lib.add( + resp_lib.GET, + f"{HOST}/programs/shield_swap_freezelist.aleo/compliance/freeze-list", + json=["0", "0", "3642222252059314292809609689035560016959342421640560347114299934615987159853"], + ) + c = make_client() + + tree = c.get_freeze_list("shield_swap_freezelist.aleo") + + assert tree == [ + 0, 0, + 3642222252059314292809609689035560016959342421640560347114299934615987159853, + ] + assert resp_lib.calls[0].request.url == ( + f"{HOST}/programs/shield_swap_freezelist.aleo/compliance/freeze-list") + + +@resp_lib.activate +def test_get_freeze_list_serves_a_different_program() -> None: + resp_lib.add( + resp_lib.GET, + f"{HOST}/programs/test_usad_freezelist.aleo/compliance/freeze-list", + json=["0", "0", "17"], + ) + c = make_client() + + assert c.get_freeze_list("test_usad_freezelist.aleo") == [0, 0, 17] + + +@resp_lib.activate +def test_get_freeze_list_rejects_a_non_list_payload() -> None: + resp_lib.add( + resp_lib.GET, + f"{HOST}/programs/shield_swap_freezelist.aleo/compliance/freeze-list", + json={"unexpected": True}, + ) + c = make_client() + + with pytest.raises(ValueError, match="freeze list"): + c.get_freeze_list("shield_swap_freezelist.aleo") diff --git a/sdk/python/tests/test_network_client_async.py b/sdk/python/tests/test_network_client_async.py index 6d4f817..6e3fb82 100644 --- a/sdk/python/tests/test_network_client_async.py +++ b/sdk/python/tests/test_network_client_async.py @@ -596,3 +596,40 @@ def handler(req: httpx.Request) -> httpx.Response: assert result["ok"] is False assert result["status"] == 400 assert post_count == 1 + + +# --------------------------------------------------------------------------- +# Freeze list (compliance) +# --------------------------------------------------------------------------- + +async def test_get_freeze_list_url_takes_the_program() -> None: + """The program is a parameter — one endpoint serves every freeze list.""" + root = "3642222252059314292809609689035560016959342421640560347114299934615987159853" + seen: dict[str, str] = {} + + def handler(request: httpx.Request) -> httpx.Response: + seen["url"] = str(request.url) + return jr(["0", "0", root]) + + c = make_client() + c._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + + tree = await c.get_freeze_list("shield_swap_freezelist.aleo") + + assert tree == [0, 0, int(root)] + assert seen["url"] == ( + f"{HOST}/programs/shield_swap_freezelist.aleo/compliance/freeze-list") + + +async def test_get_freeze_list_serves_a_different_program() -> None: + c = make_client({"test_usad_freezelist.aleo/compliance/freeze-list": + jr(["0", "0", "17"])}) + + assert await c.get_freeze_list("test_usad_freezelist.aleo") == [0, 0, 17] + + +async def test_get_freeze_list_rejects_a_non_list_payload() -> None: + c = make_client({"compliance/freeze-list": jr({"unexpected": True})}) + + with pytest.raises(ValueError, match="freeze list"): + await c.get_freeze_list("shield_swap_freezelist.aleo") diff --git a/shield-swap-sdk/tests/integration/devnode_amm.py b/shield-swap-sdk/tests/integration/devnode_amm.py index e2171fb..111454d 100644 --- a/shield-swap-sdk/tests/integration/devnode_amm.py +++ b/shield-swap-sdk/tests/integration/devnode_amm.py @@ -134,18 +134,34 @@ def wait_queryable(self, program_id: str) -> None: def deploy_program(self, source: str, label: str) -> str: """Deploy *source* proofless: dummy verifying keys (no synthesis) and - an unproven public fee paid by the admin — devnode-only.""" + an unproven public fee paid by the admin — devnode-only. + + The devnode bundles its own snarkVM, which need not price deployments + identically to the bindings computing the fee here; the two diverged at + devnode 0.2.3 / snarkVM 4.9.0. A rejected base fee names the amount the + node wants, so a short fee is retried at that figure rather than + carrying a guessed margin. + """ net = self._net() process = self.aleo.process program = net.Program.from_source(source) - deployment = net.Deployment.from_program_unproven(program, self.admin.address) + cost = process.deployment_cost(deployment) - fee_auth = process.authorize_fee_public( - self.admin.private_key, cost, 0, deployment.deployment_id()) - fee = net.Fee.from_authorization_unproven(fee_auth, self.state_root()) - tx = net.Transaction.from_deployment(self.admin.private_key, deployment, fee) - tx_id = self.submit_and_confirm(tx, f"deploy {label}") + for attempt in range(2): + fee_auth = process.authorize_fee_public( + self.admin.private_key, cost, 0, deployment.deployment_id()) + fee = net.Fee.from_authorization_unproven(fee_auth, self.state_root()) + tx = net.Transaction.from_deployment(self.admin.private_key, deployment, fee) + try: + tx_id = self.submit_and_confirm(tx, f"deploy {label}") + break + except Exception as exc: + required = re.search(r"requires (\d+) microcredits", str(exc)) + if attempt or not required: + raise + cost = int(required.group(1)) + self.wait_queryable(label) # Later deployments/executions resolve this program from the process. process.add_program(program) diff --git a/shield-swap-sdk/tests/integration/devnode_merkle_stack.py b/shield-swap-sdk/tests/integration/devnode_merkle_stack.py new file mode 100644 index 0000000..9fe1046 --- /dev/null +++ b/shield-swap-sdk/tests/integration/devnode_merkle_stack.py @@ -0,0 +1,382 @@ +"""Devnode fixture for freeze-list Merkle exclusion proofs. + +Boots ``aleo-devnode``, deploys the shield_swap stack **fetched live from the +network** rather than from repo fixtures — the deployed programs are the +authoritative statement of what the verifier accepts, and a vendored copy can +drift from them silently. Then it populates the freeze list with real +addresses and leaves the stack ready for a transition that must prove +non-inclusion. + +This is what makes the proofs meaningful: against an *empty* freeze list the +all-zero placeholder literal verifies, so only a populated list exercises +:class:`aleo.MerkleExclusionProof` for real. + +Deployment is proofless (dummy verifying keys, unproven fee), as in +``devnode_amm``: synthesizing real keys for the AMM takes many minutes and buys +nothing on a node that skips certificate verification. + +Execution ladders: + +* **unproven** (default here): the authorization still *evaluates* the + transition, so ``verify_merkle_non_inclusion`` and its asserts run in full — + only the SNARK is skipped. The finalize, including + ``assert_valid_freeze_list_root``, runs on-chain either way. Both halves of + the proof are therefore checked. +* **proven** (``ALEO_DEVNODE_MERKLE_PROVEN=1``): full local proving, much + slower. +""" +from __future__ import annotations + +import os +import re +import time +from dataclasses import dataclass, field +from typing import Any, Optional + +import requests + +from aleo import MerkleExclusionProof + +#: The node API is the source of truth for deployed program source. +NETWORK_API = os.environ.get( + "ALEO_DEVNODE_SOURCE_API", "https://api.provable.com/v2/testnet") + +AMM_PROGRAM = "shield_swap.aleo" +FREEZELIST_PROGRAM = "shield_swap_freezelist.aleo" +AMM_MULTISIG = "shield_swap_multisig_core.aleo" +TOKEN_MULTISIG = "test_arc20_multisig_core.aleo" +TOKEN0_PROGRAM = "test_arc20_usdc.aleo" +TOKEN1_PROGRAM = "test_arc20_usdt.aleo" + +#: Deployment order — imports before importers. +DEPLOY_ORDER = [ + AMM_MULTISIG, + FREEZELIST_PROGRAM, + AMM_PROGRAM, + TOKEN_MULTISIG, + TOKEN0_PROGRAM, + TOKEN1_PROGRAM, +] + +#: Administrator literals baked into the deployed programs. On a devnode these +#: keys do not exist, so every occurrence is rewritten to a genesis account. +#: The AMM and its freeze list share one; the test ARC-20s share another. +BAKED_ADMINS = [ + "aleo1z3zwzgpgakk89xpknync5rtklkjkyv33g7cvaqe0gku64zs3lv9qyux0qc", + "aleo1axurgcdhztu8m23ttzju38qzchtzs8kyk7nga9n58zyrmnxzmuqqf6wqdc", +] + +#: Freeze-list role bits: 8 grants roles, 16 edits the list. +FREEZELIST_MANAGER_ROLE = 24 +#: ARC-20 role bits: 8 grants roles, 1 mints. +TOKEN_MINTER_ROLE = 9 + +#: Blocks the previous freeze-list root stays valid after a rotation. +FREEZELIST_WINDOW = 100 +#: The devnode's snarkVM uses TEST consensus heights; the last activates at 20. +LAST_TEST_CONSENSUS_HEIGHT = 20 + +TOKEN_SUPPLY = 1_000_000_000_000 + +PROVEN = os.environ.get("ALEO_DEVNODE_MERKLE_PROVEN") == "1" + + +def identifier_to_field(identifier: str) -> str: + """A program identifier (no ``.aleo``) as the field the AMM keys tokens by. + + Little-endian bytes, verified against every entry of the live token + registry. + """ + value = 0 + for byte in reversed(identifier.encode()): + value = (value << 8) | byte + return f"{value}field" + + +def fetch_program(program_id: str) -> str: + """Deployed source for *program_id*, straight from the node API.""" + response = requests.get(f"{NETWORK_API}/program/{program_id}", timeout=60) + response.raise_for_status() + source = response.json() + if not isinstance(source, str) or f"program {program_id}" not in source: + raise RuntimeError(f"{program_id} did not come back as program source") + return source + + +def repoint_admins(source: str, admin_address: str) -> str: + """Rewrite every baked administrator literal to *admin_address*. + + Both the constructor gate and the finalize gates (``initialize``, + ``initialize_token``) compare against the same literal, so all occurrences + have to move together or the program deploys but cannot be configured. + """ + for baked in BAKED_ADMINS: + source = source.replace(baked, admin_address) + return source + + +@dataclass +class MerkleDevnode: + """A devnode running the network's shield_swap stack with a live freeze list.""" + + devnode: Any + aleo: Any + admin: Any + user: Any + sources: dict[str, str] + token0_id: str + token1_id: str + token0_program: str + token1_program: str + #: Addresses currently frozen, in insertion order. + frozen: list[str] = field(default_factory=list) + merkle: MerkleExclusionProof = field(default_factory=MerkleExclusionProof) + + # ── Chain plumbing ────────────────────────────────────────────────────── + + def _net(self) -> Any: + from aleo import testnet + return testnet + + def state_root(self) -> str: + return str(self.aleo.network.get_state_root()) + + def submit_and_confirm(self, tx: Any, label: str) -> str: + tx_id = self.aleo.network.submit_transaction(tx) + self.devnode.advance(1) + confirmed = str(self.aleo.network.get_confirmed_transaction(str(tx_id))) + if '"accepted"' not in confirmed and "'accepted'" not in confirmed: + raise RuntimeError( + f"{label}: transaction {tx_id} was not accepted: " + f"{confirmed[:400]}\ndevnode logs:\n" + + "\n".join(self.devnode.logs()[-15:]) + ) + return str(tx_id) + + def wait_queryable(self, program_id: str) -> None: + for _ in range(20): + try: + if f"program {program_id}" in str( + self.aleo.network.get_program(program_id)): + return + except Exception: + pass + self.devnode.advance(1) + time.sleep(0.3) + raise RuntimeError(f"{program_id} never became queryable") + + def deploy_program(self, program_id: str) -> str: + """Deploy proofless: dummy verifying keys and an unproven public fee. + + The devnode ships its own snarkVM, which need not price deployments + identically to the bindings computing the fee here. When the node + rejects the base fee it names the amount it wants, so a short fee is + retried at that figure rather than guessing a margin. + """ + net = self._net() + process = self.aleo.process + program = net.Program.from_source(self.sources[program_id]) + deployment = net.Deployment.from_program_unproven(program, self.admin.address) + + cost = process.deployment_cost(deployment) + for attempt in range(2): + fee_auth = process.authorize_fee_public( + self.admin.private_key, cost, 0, deployment.deployment_id()) + fee = net.Fee.from_authorization_unproven(fee_auth, self.state_root()) + tx = net.Transaction.from_deployment( + self.admin.private_key, deployment, fee) + try: + tx_id = self.submit_and_confirm(tx, f"deploy {program_id}") + break + except Exception as exc: + required = re.search(r"requires (\d+) microcredits", str(exc)) + if attempt or not required: + raise + cost = int(required.group(1)) + + self.wait_queryable(program_id) + # Registers the program for later executions and dynamic dispatch. + process.add_program(program) + return tx_id + + def execute(self, account: Any, program_id: str, function: str, + inputs: list[Any], label: str) -> str: + """Run one transition as *account* and require on-chain acceptance.""" + bound = self.aleo.programs.get(program_id).functions[function](*inputs) + + if PROVEN: + return self.submit_and_confirm( + bound.build_transaction(account).raw, label) + # authorize() evaluates the transition, so in-circuit asserts — the + # Merkle verification included — run here even though no SNARK follows. + return self.submit_and_confirm(self._unproven_tx(bound, account), label) + + def submit_call(self, call: Any, account: Any, label: str) -> str: + """Drive a ``DexCall`` through the same ladder as :meth:`execute`. + + Used for the transitions that take no Merkle proof (pool creation), so + those keep going through the shipped client rather than hand-built + inputs. + """ + if PROVEN: + result = call.transact(account) + self.devnode.advance(1) + return str(result.transaction_id) + return self.submit_and_confirm( + self._unproven_tx(call._bound, account), label) + + def _unproven_tx(self, bound: Any, account: Any) -> Any: + """A proofless execution transaction for an already-bound call.""" + net = self._net() + process = self.aleo.process + auth = bound.authorize(account).raw + root = self.state_root() + execution = net.Execution.from_authorization_unproven(auth, root) + cost, _ = process.execution_cost(execution) + fee_auth = process.authorize_fee_public( + account.private_key, cost, 0, execution.execution_id) + fee = net.Fee.from_authorization_unproven(fee_auth, root) + return net.Transaction.from_execution(execution, fee) + + def records_of(self, account: Any, tx_id: str) -> list[str]: + tx = self.aleo.network.get_transaction_object(tx_id) + return [str(r) for r in tx.owned_records(account.view_key)] + + def privatize_token(self, account: Any, token_program: str, + amount: int) -> str: + tx_id = self.execute( + account, token_program, "transfer_public_to_private", + [str(account.address), f"{amount}u128"], + f"privatize {amount} {token_program}") + records = [r for r in self.records_of(account, tx_id) if "amount" in r] + if not records: + raise RuntimeError(f"no Token record from {token_program} (tx {tx_id})") + return records[0] + + def read_mapping(self, program_id: str, mapping: str, key: str) -> Optional[str]: + from aleo_shield_swap._core import normalize_mapping_value + return normalize_mapping_value( + self.aleo.programs.get(program_id).mapping(mapping).get(key)) + + # ── Freeze list ───────────────────────────────────────────────────────── + + def on_chain_root(self) -> str: + """The freeze list's current root, as the AMM's finalize reads it.""" + root = self.read_mapping(FREEZELIST_PROGRAM, "freeze_list_root", "1u8") + if root is None: + raise RuntimeError("freeze list has no root — initialize first") + return root + + def local_tree(self) -> list[int]: + """The tree over the currently frozen addresses, built locally.""" + return self.merkle.build_tree( + self.merkle.leaves_from_addresses(self.frozen)) + + def freeze(self, address: str) -> str: + """Add *address* to the freeze list, rotating the root to match. + + The contract stores whatever root the manager hands it, so this is + where the locally computed root becomes the one every later proof must + reproduce. Entries occupy indices from 1 upward; index 0 is the zero + address written at initialization. + """ + previous_root = self.on_chain_root() + self.frozen.append(address) + new_root = self.merkle.root(self.local_tree()) + tx_id = self.execute( + self.admin, FREEZELIST_PROGRAM, "update_freeze_list", + [address, "true", f"{len(self.frozen)}u32", previous_root, new_root], + f"freeze {address[:12]}…") + return tx_id + + def exclusion_proof(self, address: str) -> str: + """A ``[MerkleProof; 2]`` literal proving *address* is not frozen.""" + return self.merkle.exclusion_proof(self.local_tree(), address) + + def stop(self) -> None: + self.devnode.stop() + + +def setup_merkle_devnode(freeze_count: int = 5) -> MerkleDevnode: + """Boot a devnode with the network's stack deployed and a populated list. + + Args: + freeze_count: How many generated addresses to put on the freeze list. + Anything above zero makes the placeholder proof invalid, which is + the point. + """ + from aleo.testing import Devnode + + devnode = Devnode().start() + aleo = devnode.aleo + admin = devnode.accounts[0] + aleo.default_account = admin + aleo.record_provider = None # no scanner on a devnode + devnode.advance(LAST_TEST_CONSENSUS_HEIGHT + 2) + + sources = { + pid: repoint_admins(fetch_program(pid), str(admin.address)) + for pid in DEPLOY_ORDER + } + + usdc_id = identifier_to_field(TOKEN0_PROGRAM.removesuffix(".aleo")) + usdt_id = identifier_to_field(TOKEN1_PROGRAM.removesuffix(".aleo")) + # Pools order their sides by token id, not by name. + usdc_first = int(usdc_id.removesuffix("field")) < int(usdt_id.removesuffix("field")) + + ctx = MerkleDevnode( + devnode=devnode, aleo=aleo, admin=admin, user=None, sources=sources, + token0_id=usdc_id if usdc_first else usdt_id, + token1_id=usdt_id if usdc_first else usdc_id, + token0_program=TOKEN0_PROGRAM if usdc_first else TOKEN1_PROGRAM, + token1_program=TOKEN1_PROGRAM if usdc_first else TOKEN0_PROGRAM, + ) + + for program_id in DEPLOY_ORDER: + ctx.deploy_program(program_id) + + # Freeze list: initialize grants the admin role 8 (grant roles only), so it + # has to promote itself to 24 before it can edit the list. + ctx.execute(admin, FREEZELIST_PROGRAM, "initialize", + [str(admin.address), f"{FREEZELIST_WINDOW}u32"], + "freezelist initialize") + ctx.execute(admin, FREEZELIST_PROGRAM, "update_role", + [str(admin.address), f"{FREEZELIST_MANAGER_ROLE}u16"], + "freezelist grant manager role") + + # Tokens: same shape — initialize grants role 8, minting needs bit 1. + for token_program in (TOKEN0_PROGRAM, TOKEN1_PROGRAM): + ctx.execute(admin, token_program, "initialize_token", + [str(admin.address)], f"initialize {token_program}") + ctx.execute(admin, token_program, "update_role", + [str(admin.address), f"{TOKEN_MINTER_ROLE}u16"], + f"grant minter on {token_program}") + + # AMM admin configuration, mirroring the deployed testnet parameters. + for label, function, inputs in [ + ("fee tier 3000", "add_fee_tier", ["3000u16"]), + ("tick spacing 60", "add_tick_spacing", ["60u32"]), + ("bind 3000->60", "bind_fee_to_tick_spacing", ["3000u16", "60u32"]), + ("allow token0", "allow_token", [ctx.token0_id, ctx.token0_id]), + ("allow token1", "allow_token", [ctx.token1_id, ctx.token1_id]), + ("open pool creation", "set_pool_creation_is_open", ["true"]), + ]: + ctx.execute(admin, AMM_PROGRAM, function, inputs, f"admin {label}") + + # A funded non-admin does the proving work, so the signer under test is not + # the same account that administers the list. + user = aleo.account.create() + ctx.user = user + ctx.execute(admin, "credits.aleo", "transfer_public", + [str(user.address), "100000000u64"], "fund user") + for token_program in (TOKEN0_PROGRAM, TOKEN1_PROGRAM): + ctx.execute(admin, token_program, "mint_public", + [str(user.address), f"{TOKEN_SUPPLY}u128"], + f"mint {token_program} to user") + + # Populate the list with addresses that are NOT the user, so the user can + # still prove non-inclusion against a genuinely non-empty tree. + for _ in range(freeze_count): + ctx.freeze(str(aleo.account.create().address)) + + return ctx diff --git a/shield-swap-sdk/tests/integration/test_devnode_merkle.py b/shield-swap-sdk/tests/integration/test_devnode_merkle.py new file mode 100644 index 0000000..5194557 --- /dev/null +++ b/shield-swap-sdk/tests/integration/test_devnode_merkle.py @@ -0,0 +1,218 @@ +"""Freeze-list Merkle exclusion proofs against the real deployed AMM. + +Deploys the network's ``shield_swap.aleo``, ``shield_swap_freezelist.aleo``, +and two plain ARC-20s onto a devnode, populates the freeze list, and mints a +position — the transition that requires three separate non-inclusion proofs +(signer, recipient, withdrawal). + +The unit tests in ``sdk/python/tests/test_merkle.py`` check the port against a +Python transcription of the verifier. This checks it against the verifier +itself, compiled into the deployed bytecode, which is the only thing that +settles whether the proofs are actually right. + +``test_placeholder_proof_is_rejected`` is the control: it shows the all-zero +literal the SDK ships today stops working the moment the list is non-empty, so +the passing mint is evidence about the proofs rather than about a list that +happens to accept anything. + +Run with:: + + pytest tests/integration/test_devnode_merkle.py -m devnode +""" +from __future__ import annotations + +import pytest + +from aleo import MerkleExclusionProof +from aleo_shield_swap import ShieldSwap +from aleo_shield_swap import _generated as g +from aleo_shield_swap._core import default_merkle_proofs, generate_field_nonce + +from .devnode_merkle_stack import ( + AMM_PROGRAM, + FREEZELIST_PROGRAM, + MerkleDevnode, + setup_merkle_devnode, +) + +pytestmark = pytest.mark.devnode + +FEE = 3000 +INITIAL_TICK = 0 +MINT_AMOUNT = 100_000_000 +FUND_AMOUNT = 200_000_000 + +#: ``Poseidon4([1field, 0field, 0field])`` — the root while the list is empty. +EMPTY_TREE_ROOT = ( + "3642222252059314292809609689035560016959342421640560347114299934615987159853field" +) + + +@pytest.fixture(scope="module") +def ctx() -> MerkleDevnode: + try: + context = setup_merkle_devnode(freeze_count=5) + except Exception as exc: + if "aleo-devnode not found" in str(exc): + pytest.skip(f"aleo-devnode binary not available: {exc}") + raise + yield context + context.stop() + + +@pytest.fixture(scope="module") +def dex(ctx: MerkleDevnode) -> ShieldSwap: + client = ShieldSwap(ctx.aleo, program=AMM_PROGRAM) + ctx.aleo.default_account = ctx.user + return client + + +@pytest.fixture(scope="module") +def pool_key(ctx: MerkleDevnode, dex: ShieldSwap) -> str: + """A live pool over the two deployed ARC-20s.""" + call = dex.create_pool( + token0_id=ctx.token0_id, token1_id=ctx.token1_id, + fee=FEE, initial_tick=INITIAL_TICK, account=ctx.user) + ctx.submit_call(call, ctx.user, "create_pool") + key = dex.derive_pool_key(ctx.token0_id, ctx.token1_id, FEE) + assert ctx.read_mapping(AMM_PROGRAM, "pools", key), "pool was not created" + return key + + +def build_mint_inputs(ctx: MerkleDevnode, dex: ShieldSwap, pool_key: str, *, + signer_proofs: str, recipient_proofs: str, + withdrawal_proofs: str) -> list[str]: + """``mint`` inputs in deployed order, with the proofs left to the caller. + + Mirrors ``ShieldSwap.mint``'s assembly. It is rebuilt here rather than + reused because the shipped method hardcodes the placeholder proof and takes + no parameter to override it. + """ + slot = dex.get_slot(pool_key) + spacing = slot.tick_spacing + lower, upper = -10 * spacing, 10 * spacing + + lower_hint = dex.find_tick_predecessor(pool_key, lower) + upper_pred = dex.find_tick_predecessor(pool_key, upper) + # The finalize inserts the lower tick before validating the upper hint. + upper_hint = lower if lower > upper_pred else upper_pred + + request = g.MintPositionRequest( + pool=pool_key, tick_lower=lower, tick_upper=upper, + amount0_desired=MINT_AMOUNT, amount1_desired=MINT_AMOUNT, + amount0_min=0, amount1_min=0, + tick_lower_hint=lower_hint, tick_upper_hint=upper_hint, + ).to_plaintext() + + record0 = ctx.privatize_token(ctx.user, ctx.token0_program, FUND_AMOUNT) + record1 = ctx.privatize_token(ctx.user, ctx.token1_program, FUND_AMOUNT) + + return [ + generate_field_nonce(), record0, record1, + str(ctx.user.address), str(ctx.user.address), request, + ctx.token0_id, ctx.token1_id, + signer_proofs, recipient_proofs, withdrawal_proofs, + ] + + +def test_freeze_list_root_matches_the_locally_built_tree(ctx: MerkleDevnode): + """The root the contract stores is the one the port computes. + + Nothing on chain recomputes the root — the manager supplies it — so this is + the step that ties the local tree to the value every proof must reproduce. + """ + assert len(ctx.frozen) == 5 + assert ctx.on_chain_root() == ctx.merkle.root(ctx.local_tree()) + + +def test_freeze_list_is_no_longer_empty(ctx: MerkleDevnode): + """Without this, a passing mint would prove nothing about the proofs.""" + assert ctx.on_chain_root() != EMPTY_TREE_ROOT + + +def test_frozen_addresses_cannot_be_proven_absent(ctx: MerkleDevnode): + """A member is rejected client-side rather than on-chain.""" + with pytest.raises(ValueError, match="on the list"): + ctx.exclusion_proof(ctx.frozen[0]) + + +def test_placeholder_proof_is_rejected(ctx: MerkleDevnode, dex: ShieldSwap, + pool_key: str): + """The all-zero literal fails against a populated list. + + This is the control for :func:`test_mint_with_exclusion_proofs_succeeds`: + without it, a mint could pass because the list accepts anything. + + The placeholder clears the *circuit* — two copies of a depth-1 all-zero + path reconstruct the empty-tree root, and every real address sorts above + ``0field``, so it is a genuine non-inclusion proof for an empty tree. What + stops it is the finalize's ``assert_valid_freeze_list_root``, which no + longer matches once the list has entries. The transaction is therefore + *rejected* on chain rather than failing to authorize. + """ + placeholder = default_merkle_proofs() + inputs = build_mint_inputs( + ctx, dex, pool_key, signer_proofs=placeholder, + recipient_proofs=placeholder, withdrawal_proofs=placeholder) + + with pytest.raises(RuntimeError, match="was not accepted") as excinfo: + ctx.execute(ctx.user, AMM_PROGRAM, "mint", inputs, + "mint with placeholder proofs") + + assert "'status': 'rejected'" in str(excinfo.value), ( + "expected the finalize's root check to reject the placeholder") + + +def test_mint_with_exclusion_proofs_succeeds(ctx: MerkleDevnode, dex: ShieldSwap, + pool_key: str): + """A real proof carries a mint through the deployed verifier. + + The signer, recipient, and withdrawal address are all the user here, so the + same proof satisfies all three checks — the contract verifies each + independently and requires all three roots to agree. + """ + proof = ctx.exclusion_proof(str(ctx.user.address)) + inputs = build_mint_inputs( + ctx, dex, pool_key, signer_proofs=proof, + recipient_proofs=proof, withdrawal_proofs=proof) + + tx_id = ctx.execute(ctx.user, AMM_PROGRAM, "mint", inputs, + "mint with exclusion proofs") + + assert tx_id + nfts = [r for r in ctx.records_of(ctx.user, tx_id) if "token_id" in r] + assert nfts, "mint produced no PositionNFT" + + +def test_proof_still_verifies_after_the_list_grows(ctx: MerkleDevnode, + dex: ShieldSwap, + pool_key: str): + """Freezing another address rotates the root; a fresh proof tracks it.""" + before = ctx.on_chain_root() + ctx.freeze(str(ctx.aleo.account.create().address)) + after = ctx.on_chain_root() + assert after != before, "freezing did not rotate the root" + + proof = ctx.exclusion_proof(str(ctx.user.address)) + inputs = build_mint_inputs( + ctx, dex, pool_key, signer_proofs=proof, + recipient_proofs=proof, withdrawal_proofs=proof) + + assert ctx.execute(ctx.user, AMM_PROGRAM, "mint", inputs, + "mint after root rotation") + + +def test_exclusion_proof_from_the_served_tree(ctx: MerkleDevnode): + """A tree read back as serialized nodes proves the same as a built one. + + This is the shape ``AleoNetworkClient.get_freeze_list`` returns, so it + covers the path a caller takes when the list comes from a service rather + than from a local rebuild. + """ + built = ctx.local_tree() + + served = MerkleExclusionProof().tree_from_nodes([str(n) for n in built]) + + assert served == built + assert (MerkleExclusionProof().exclusion_proof(served, str(ctx.user.address)) + == ctx.exclusion_proof(str(ctx.user.address)))