From 013408b62fcc27c6b34cf7b874c10ec9454d7d70 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 10:16:31 -0500 Subject: [PATCH 01/72] build: bump snarkvm to 4.7.3, add mainnet/testnet feature seam Co-Authored-By: Claude Fable 5 --- sdk/Cargo.lock | 2503 +++++++++++++++++++++++++++++++----------------- sdk/Cargo.toml | 9 +- 2 files changed, 1638 insertions(+), 874 deletions(-) diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index 2e5700a..4ad2368 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -18,25 +18,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] -name = "ahash" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -dependencies = [ - "memchr", -] +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aleo" @@ -47,7 +32,7 @@ dependencies = [ "once_cell", "openssl", "pyo3", - "rand", + "rand 0.8.5", "serde", "serde_json", "snarkvm", @@ -55,9 +40,9 @@ dependencies = [ [[package]] name = "aleo-std" -version = "0.1.18" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3aa6ad1a3bb96698e7e8d8e42a6f2fda3b8611a43aab4d5effb921f18798833" +checksum = "869be259eeb00852087ad36e9a68c67959c2811dc8fc6c680fcac98948c4f172" dependencies = [ "aleo-std-cpu", "aleo-std-profiler", @@ -65,56 +50,58 @@ dependencies = [ "aleo-std-time", "aleo-std-timed", "aleo-std-timer", + "walkdir", ] [[package]] name = "aleo-std-cpu" -version = "0.1.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7527351aa675fdbe6a1902de3cf913ff7d50ccd6822f1562be374bdd85eaefb8" +checksum = "9881417111e9266cf47bb2fec0e7ef32454fa2ac05763a48f25c1d50f260e1b9" [[package]] name = "aleo-std-profiler" -version = "0.1.15" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf055ddb2f54fa86394d19d87e7956df2f3cafff489fc14c0f48f2f80664c3d" +checksum = "0d53500befe6ecd23f8fc11ccf510b680516b3a6aff08d60f12e1dda00b77787" [[package]] name = "aleo-std-storage" -version = "0.1.3" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503e2538d5158b869bc9c30c9754f9a23f4210987008014a9f118db99f22c217" +checksum = "46f6571ae16ee20c1e9a95b6ba685ca7156d519400f597c5e59f53b641637225" dependencies = [ "dirs", + "tempfile", ] [[package]] name = "aleo-std-time" -version = "0.1.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f2a841f04c2eaeb5a95312e5201a9e4b7c95b64ca99870d6bd2e2376df540a" +checksum = "a9ebd144c81671193ed85aa2db9bb5e183421843e0485de8fffc07e5cf50e18a" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.46", "syn 1.0.109", ] [[package]] name = "aleo-std-timed" -version = "0.1.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6118baab6285accf088b31d5ea5029c37bbf9d98e62b4d8720a0a5a66bc2e427" +checksum = "68f6ff9e4c36858fa2c29e5284b77527b5a7466743976e1ba1f5824e16683545" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.46", "syn 1.0.109", ] [[package]] name = "aleo-std-timer" -version = "0.1.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4f181fc1a372e8ceff89612e5c9b13f72bff5b066da9f8d6827ae65af492c4" +checksum = "12aca1021aef2c476bad30d2f681e891b2be4f07dbc230a96df09cb693bfb3cb" [[package]] name = "allocator-api2" @@ -122,59 +109,14 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" -[[package]] -name = "anstream" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anstyle-parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +dependencies = [ + "backtrace", +] [[package]] name = "arrayref" @@ -195,8 +137,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -205,6 +147,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -215,22 +180,34 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.1", "object", "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" -version = "0.21.5" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bech32" -version = "0.9.1" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bincode" @@ -249,9 +226,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "blake2" @@ -259,7 +236,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -282,6 +259,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bs58" version = "0.5.0" @@ -305,17 +291,20 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.0.83" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ + "find-msvc-tools", + "jobserver", "libc", + "shlex", ] [[package]] @@ -325,86 +314,102 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "clap" -version = "4.4.10" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" -dependencies = [ - "clap_builder", - "clap_derive", -] +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "clap_builder" -version = "4.4.9" +name = "chacha20" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", ] [[package]] -name = "clap_derive" -version = "4.4.7" +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "heck", - "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "cc", ] [[package]] -name = "clap_lex" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" - -[[package]] -name = "colorchoice" -version = "1.0.0" +name = "cmov" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "colored" -version = "2.0.4" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "is-terminal", - "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] -name = "console" -version = "0.15.7" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.45.0", + "bytes", + "memchr", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna 1.1.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -412,15 +417,21 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] @@ -473,6 +484,21 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -484,54 +510,41 @@ dependencies = [ ] [[package]] -name = "curl" -version = "0.4.44" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2 0.4.10", - "winapi", + "hybrid-array", + "rand_core 0.10.1", ] [[package]] -name = "curl-sys" -version = "0.4.68+curl-8.4.0" +name = "ctutils" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "windows-sys 0.48.0", + "cmov", + "subtle", ] [[package]] -name = "deranged" -version = "0.3.10" +name = "der" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" dependencies = [ - "powerfmt", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "derivative" -version = "2.2.0" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote 1.0.33", - "syn 1.0.109", -] +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] name = "digest" @@ -539,11 +552,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "4.0.0" @@ -565,10 +590,44 @@ dependencies = [ ] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest 0.11.3", + "elliptic-curve", + "rfc6979", + "signature", + "zeroize", +] [[package]] name = "either" @@ -577,18 +636,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] -name = "encode_unicode" -version = "0.3.6" +name = "elliptic-curve" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff", + "group", + "hybrid-array", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] [[package]] -name = "encoding_rs" -version = "0.8.33" +name = "enum-iterator" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" dependencies = [ - "cfg-if", + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -615,9 +698,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.52.0", @@ -625,34 +708,47 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "instant", + "rand_core 0.10.1", + "subtle", ] [[package]] -name = "fastrand" -version = "2.0.1" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.0.28" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "foreign-types" @@ -678,6 +774,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.29" @@ -693,18 +795,6 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" -[[package]] -name = "futures-io" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" - -[[package]] -name = "futures-sink" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" - [[package]] name = "futures-task" version = "0.3.29" @@ -718,12 +808,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", - "futures-io", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -752,8 +839,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -763,37 +866,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] -name = "h2" -version = "0.3.22" +name = "group" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "ff", + "rand_core 0.10.1", + "subtle", ] [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash 0.1.5", ] [[package]] -name = "heck" -version = "0.4.1" +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" @@ -809,75 +922,219 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "http" -version = "0.2.11" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", - "fnv", "itoa", ] [[package]] name = "http-body" -version = "0.4.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "httpdate" -version = "1.0.3" +name = "hybrid-array" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] [[package]] name = "hyper" -version = "0.14.27" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", - "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.4", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] @@ -890,87 +1147,150 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" -version = "2.1.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "rayon", "serde", + "serde_core", ] [[package]] -name = "indicatif" -version = "0.17.7" +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", + "either", ] [[package]] -name = "indoc" -version = "2.0.4" +name = "itoa" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] -name = "instant" -version = "0.1.12" +name = "jni" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", ] [[package]] -name = "ipnet" -version = "2.9.0" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.48.0", + "jni-sys-macros", ] [[package]] -name = "itertools" -version = "0.11.0" +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ - "either", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "itoa" -version = "1.0.9" +name = "jobserver" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "once_cell", "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.14.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ce7f1aa9a24c53c6572d8017c8c1ceb5d44c6071ff68c9912860fa4d262101" +dependencies = [ + "cpubits", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", + "wnaf", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -979,9 +1299,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" @@ -989,28 +1309,28 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.13.0", "libc", "redox_syscall", ] [[package]] -name = "libz-sys" -version = "1.1.12" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "linux-raw-sys" -version = "0.4.12" +name = "litemap" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -1024,9 +1344,24 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "memchr" @@ -1043,12 +1378,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1064,6 +1393,16 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.9" @@ -1077,11 +1416,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -1103,6 +1441,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -1114,6 +1461,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.1" @@ -1121,18 +1474,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", -] - -[[package]] -name = "num-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" -dependencies = [ - "arrayvec", - "itoa", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -1147,9 +1490,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1164,12 +1507,6 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "object" version = "0.32.1" @@ -1181,21 +1518,20 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl" -version = "0.10.60" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.13.0", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -1207,21 +1543,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.96" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -1258,6 +1594,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1278,15 +1623,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "portable-atomic" -version = "1.5.1" +name = "potential_utf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -1300,11 +1648,36 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "primefield", + "wnaf", +] + [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1355,8 +1728,8 @@ checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" dependencies = [ "proc-macro2", "pyo3-macros-backend", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -1367,17 +1740,65 @@ checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" dependencies = [ "heck", "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "quick-xml" -version = "0.23.1" +name = "quinn" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ - "memchr", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.5", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.5", + "tracing", + "windows-sys 0.52.0", ] [[package]] @@ -1388,13 +1809,19 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -1402,18 +1829,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -1422,23 +1870,38 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.11", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", ] [[package]] name = "rand_xorshift" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" dependencies = [ - "rand_core", + "rand_core 0.10.1", ] [[package]] name = "rayon" -version = "1.8.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1446,9 +1909,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1469,76 +1932,59 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom", + "getrandom 0.2.11", "libredox", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "thiserror 1.0.50", ] -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - [[package]] name = "reqwest" -version = "0.11.22" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-core", - "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", - "system-configuration", + "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", +] + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", ] [[package]] @@ -1548,7 +1994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" dependencies = [ "cc", - "getrandom", + "getrandom 0.2.11", "libc", "spin", "untrusted", @@ -1561,6 +2007,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1572,11 +2024,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.26" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -1585,31 +2037,86 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.9" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ + "aws-lc-rs", "log", + "once_cell", "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", "rustls-webpki", - "sct", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", ] +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", + "rustls-pki-types", "untrusted", ] [[package]] -name = "ryu" -version = "1.0.15" +name = "rustversion" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "same-file" @@ -1622,11 +2129,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1636,22 +2143,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "sct" -version = "0.7.1" +name = "sec1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ - "ring", - "untrusted", + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", ] [[package]] name = "security-framework" -version = "2.9.2" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.0", "core-foundation", "core-foundation-sys", "libc", @@ -1660,105 +2171,128 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", ] [[package]] -name = "self-replace" -version = "1.3.7" +name = "semver" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525db198616b2bcd0f245daf7bfd8130222f7ee6af9ff9984c19a61bf1160c55" -dependencies = [ - "fastrand 1.9.0", - "tempfile", - "windows-sys 0.48.0", -] +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] -name = "self_update" -version = "0.38.0" +name = "seq-macro" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b3c585a1ced6b97ac13bd5e56f66559e5a75f477da5913f70df98e114518446" -dependencies = [ - "hyper", - "indicatif", - "log", - "quick-xml", - "regex", - "reqwest", - "self-replace", - "semver", - "serde_json", - "tempfile", - "urlencoding", -] +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] -name = "semver" -version = "1.0.20" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] [[package]] -name = "serde" -version = "1.0.193" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap", "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "sha2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "cfg-if", + "cpufeatures", + "digest 0.11.3", ] [[package]] -name = "sha2" -version = "0.10.8" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.9" @@ -1770,9 +2304,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smol_str" @@ -1785,54 +2319,39 @@ dependencies = [ [[package]] name = "snarkvm" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f70f657012f42ae5e0004222ddeb896d4d949f530d3ef18ae00506ab35a00c" +checksum = "d2051aaa63e873040259b906bfdf0e196b7fad78548bf117d50b0677fa9cc5f6" dependencies = [ - "anstyle", "anyhow", - "clap", - "colored", - "dotenvy", - "indexmap", - "num-format", - "once_cell", - "parking_lot", - "rand", - "rayon", - "self_update", - "serde_json", + "rand 0.10.2", + "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", "snarkvm-ledger", "snarkvm-parameters", "snarkvm-synthesizer", "snarkvm-utilities", - "thiserror", - "ureq", "walkdir", ] [[package]] name = "snarkvm-algorithms" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08da1e033ccc98ae6b3a36d58d480ff50337d7966cc1790f4cab49e41e94663f" +checksum = "cb463ee0dbf27520e3b885239da9539d1817bef54414e166abecf53e89bd9182" dependencies = [ "aleo-std", "anyhow", "blake2", "cfg-if", "fxhash", - "hashbrown", + "hashbrown 0.15.5", "hex", "indexmap", "itertools", "num-traits", - "parking_lot", - "rand", - "rand_chacha", - "rand_core", + "rand 0.10.2", "rayon", "serde", "sha2", @@ -1841,14 +2360,14 @@ dependencies = [ "snarkvm-fields", "snarkvm-parameters", "snarkvm-utilities", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "snarkvm-circuit" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd220c3d8c0bec7653f4e3bbb99f0794364b049dfc582ccc6c812d95e61eb3b" +checksum = "bb79631afb088804f48b2b5c553d80003d2d29d164ebdd72b4ae2805167dd294" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -1861,11 +2380,10 @@ dependencies = [ [[package]] name = "snarkvm-circuit-account" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95a3a044e0ff6ccab0af918d0a1abd56cabc883c8c25186521eb66690f8292b" +checksum = "86ec5f1223fb704743fb493b810ff7827f97b14ba6522b2a30863c7ed9b8c419" dependencies = [ - "snarkvm-circuit-algorithms", "snarkvm-circuit-network", "snarkvm-circuit-types", "snarkvm-console-account", @@ -1873,9 +2391,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-algorithms" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d89a4a5aa9b3c5f615c8d497bd2567746a40e9276eaf9c686af65d083b3f4c" +checksum = "bbbbdf99a4b98096477aa642e87f3c032de69dd312120c71557bd3715f7a6ca7" dependencies = [ "snarkvm-circuit-types", "snarkvm-console-algorithms", @@ -1884,9 +2402,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-collections" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5801379acc9012ceb333d34acea819fd2064f106182ff6dd894d1a42ae23a5" +checksum = "4d84b57411579eddf10b9c844874188fa6a1ef461dd195e1004fc0b8b80b8851" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-types", @@ -1895,34 +2413,36 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81eb5bfd6f50ec6370f64087af759dd56a56a52bceb9419098ec6eea3e36ec6f" +checksum = "ab81d401f337875ebfd9a33551b4ac81e2c79d70309b37e661551df4e756c8ab" dependencies = [ + "anyhow", "indexmap", "itertools", "nom", "num-traits", - "once_cell", + "smallvec", "snarkvm-algorithms", "snarkvm-circuit-environment-witness", "snarkvm-console-network", "snarkvm-curves", "snarkvm-fields", "snarkvm-utilities", + "thiserror 2.0.18", ] [[package]] name = "snarkvm-circuit-environment-witness" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2345f76b90c2befccebbd16b41fa240d2364b4c362087c2719bc3d37eb271e1" +checksum = "19c9e7e5832d167a90d02f2a3dbb329a35ac434d828dfa5a303201ee98f20458" [[package]] name = "snarkvm-circuit-network" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a0f7b1cbb1ee0b3d47e3096e8bafe8045844f1f0b8f034e39d76a93ef772e8" +checksum = "c0f3d2413bc19683240cfb561417b90fd45dd7104c82318dae5aa6f4789e49bf" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-collections", @@ -1932,11 +2452,10 @@ dependencies = [ [[package]] name = "snarkvm-circuit-program" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb96db67935aaf492e87d85c78b58d78c40871760e03bbf63bcf5893be904b" +checksum = "336325d1cb446c27a0e519c46671efb4d0afeae5509de142b5225741ad34f6f9" dependencies = [ - "paste", "snarkvm-circuit-account", "snarkvm-circuit-algorithms", "snarkvm-circuit-collections", @@ -1948,9 +2467,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e12119948f0e8ac2e1452e9f3f53ef1ee84638f5b61ba3ab9c0402f91cfddf" +checksum = "51eb87b289e03b4fa2d6d1d2ae1a731e2471c19916690bd3881e838e5c5f2a0c" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-address", @@ -1964,9 +2483,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-address" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06993421e661bd92f5726096a52bb5a8c73fd637ef5d2c7cb160e9c80db5f43c" +checksum = "a67f19e8cc472228e6d502c194ee0d40ac8e55f6d6796c4a9de19867f8790310" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -1978,9 +2497,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-boolean" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d47a776246d84d448a533f944c64811326f766f8fa6ec1960faee702e5ab9985" +checksum = "62da3b39df237d0bc23db92a77356ae9d78e3030254a777132f7ffa1e361cfaa" dependencies = [ "snarkvm-circuit-environment", "snarkvm-console-types-boolean", @@ -1988,9 +2507,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-field" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51d328748ed6d9808528a7ad5dc83c481d8b32a34883e1b1d0e7e2a2694c3e3" +checksum = "d791b3f4c37906ebe8dc22f4e9b0596e7d3498619f8654da631cc1494f9ad5dc" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -1999,9 +2518,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-group" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd190b09af0318ce1c130332b163729a2228fb1bed52a63b25a68127cdfa7d8" +checksum = "0ac8d680858600c493c592cac496fb0f6ae3542845d79cdf71059dbe4189f8b3" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2012,9 +2531,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-integers" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b00c8695658f89b4a304773969078b93b357b8b71768c62bc1078729f8e008" +checksum = "4f3ca8f3eae95acb4484f3e1b23dab4a979e10db8fc1dadd8f156413ce8303fa" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2025,9 +2544,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-scalar" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b56fdec9bd5575123959d7543a04d032a91121efff4b3cd7c482ce3c4e065202" +checksum = "875d94a9ebdb15b18f0141a6fd1abfc08d7c4268d08b2729d9343dd7e53557c1" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2037,9 +2556,9 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-string" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2080f787779a1f6930ad596f7d4f2af5dfe111fe9f0109614c1098d769b6ebb1" +checksum = "f43e2f1ffdf6cc1bedb1d0e97388096e17a9376ff387f787172caaa1a163642d" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2050,9 +2569,9 @@ dependencies = [ [[package]] name = "snarkvm-console" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8281defbca21fd8a5dc652d7cad21a34ad1b4522d818d530693d6051b9d6eeef" +checksum = "0471631ca8ca82c66a21a41dc83691ac949423564eeb67eb0b84a29e9d6b3983" dependencies = [ "snarkvm-console-account", "snarkvm-console-algorithms", @@ -2064,9 +2583,9 @@ dependencies = [ [[package]] name = "snarkvm-console-account" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c360c5389bfc658a5e0765bd9dbf4a1633ac549bf169574fd7d7f8708081c641" +checksum = "752db19fec19734761c805077da1e80e3ef5e68fac0ab94ebbf2c67559780fce" dependencies = [ "bs58", "snarkvm-console-network", @@ -2076,11 +2595,14 @@ dependencies = [ [[package]] name = "snarkvm-console-algorithms" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd97c40f77f0860e51fb776fd81759b288be94b28d46169613e9be5f25f4c92" +checksum = "eeeab6a2a88ecff12d1f146bd652ad4445dcd2fa4f429235e50a7b634fa08927" dependencies = [ "blake2s_simd", + "hex", + "k256", + "serde", "smallvec", "snarkvm-console-types", "snarkvm-fields", @@ -2090,27 +2612,28 @@ dependencies = [ [[package]] name = "snarkvm-console-collections" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fe802bdcdd7b1f75fc6937a5645afb92532b7fc4498040747c82125dab5b58" +checksum = "60bd7a8bb5498077596820b1659296d09bb47165d14512d8e6a77bb37c2e7d4c" dependencies = [ "aleo-std", + "parking_lot", "rayon", + "serde", "snarkvm-console-algorithms", "snarkvm-console-types", ] [[package]] name = "snarkvm-console-network" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27ab3d078f3b5b766cd2cfe20b18b2447e3bb6a16c7a862462373c5db703ce" +checksum = "7d9820c26fc45b4ecc4428363d865679a06bbaf20f16d243989558dc4f3a6b6a" dependencies = [ "anyhow", + "enum-iterator", "indexmap", - "itertools", "lazy_static", - "once_cell", "paste", "serde", "snarkvm-algorithms", @@ -2119,23 +2642,21 @@ dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types", "snarkvm-curves", - "snarkvm-fields", "snarkvm-parameters", - "snarkvm-utilities", ] [[package]] name = "snarkvm-console-network-environment" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debdf438642b0ee5a20f1951fc5efdf2681ec328b6c77ff4f29b95aff767da38" +checksum = "5a68a723c76f588353bfc6c32d23784a93afbdde70232eece87721e3b9667718" dependencies = [ "anyhow", "bech32", "itertools", "nom", "num-traits", - "rand", + "rand 0.10.2", "serde", "snarkvm-curves", "snarkvm-fields", @@ -2145,17 +2666,17 @@ dependencies = [ [[package]] name = "snarkvm-console-program" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced83a7d55de400a9ed8f304cb62522e548610781ec28119858e94f38503623" +checksum = "2e08eea78d9bd65da1b6500a03c5eb46fca629598449f2e5f8ddf7f6c5b497d9" dependencies = [ + "enum-iterator", "enum_index", "enum_index_derive", "indexmap", "num-derive", "num-traits", - "once_cell", - "paste", + "seq-macro", "serde_json", "snarkvm-console-account", "snarkvm-console-algorithms", @@ -2167,9 +2688,9 @@ dependencies = [ [[package]] name = "snarkvm-console-types" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5e3d4940b1acfe25d0cc7003268715ba4f575b78bad2365347c385c3b3834c" +checksum = "e5c1d0cc78b332ab23e3caecd78384cba7739588fc347d59e1bc4b8f6781c938" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-address", @@ -2183,9 +2704,9 @@ dependencies = [ [[package]] name = "snarkvm-console-types-address" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d0fb70f75c87a6cb866f1e00a6dd031fbd0adcd5172934efa1c8dfd06c1fef" +checksum = "59e784ce5f6ae527e47daa30257bfbc03a6faa07a58b56980d50096bdb17cdf4" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2195,18 +2716,18 @@ dependencies = [ [[package]] name = "snarkvm-console-types-boolean" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2957ce2dbb9632669e0310fcaabe9a9dd680d5979f4c3f965c3745826d2c333f" +checksum = "f37774ccb2e135e81210a00b2ac7a7e831bee8dc77f705b4dd1400784fffb1cb" dependencies = [ "snarkvm-console-network-environment", ] [[package]] name = "snarkvm-console-types-field" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925bed973c50b7d04848bde04d29e57b1ebb1c590ef959f31c0a3a70f9843cdb" +checksum = "44fdb611ced23d55453a6607aa1f5742c21791e320c53173f9b6c89cc99915fc" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2215,9 +2736,9 @@ dependencies = [ [[package]] name = "snarkvm-console-types-group" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d734bd39060eaa3aeec0997b8f9755040aee4cdf3e2e225666d8888164b58f0" +checksum = "adab0b7f3d8e906be46d4a4a705401d179d75b6c7c507a54e1ac21794d58d7f6" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2227,9 +2748,9 @@ dependencies = [ [[package]] name = "snarkvm-console-types-integers" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "932fb17064983820362c6b757ffeb4029a0c2b80a918b01af6b0306d3dcd6a6b" +checksum = "6ce77a6d7ad511e10cbf15e413278f7c55cc98cd9f8cc97bf9a2a97949a356e8" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2239,9 +2760,9 @@ dependencies = [ [[package]] name = "snarkvm-console-types-scalar" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035afa7b185a05f68e0fc0d339f6ce2b84e323c431944aeb241d031a34a435ec" +checksum = "26872058a807c86fc29be004db89d06061063bf0669cede9598153948a46db18" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2251,9 +2772,9 @@ dependencies = [ [[package]] name = "snarkvm-console-types-string" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ed979bc56a3938bcaf6444fe36d52ce1fcb3b526173a9d5c8fa99c75c91c438" +checksum = "0e98c7497bb3861a1610a6987edf5b3e79b76c249209252baeb1767a8e238e51" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2263,71 +2784,74 @@ dependencies = [ [[package]] name = "snarkvm-curves" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "012f2e0576f44ec165b5f46accfe9960619bb88acf115a247ebbb4d5ab37131a" +checksum = "9bb85f50dc014ca37d78e91187e3ed6f2c5f36c505656841647c8a7d16d3951e" dependencies = [ - "rand", - "rayon", + "rand 0.10.2", "rustc_version", "serde", "snarkvm-fields", "snarkvm-utilities", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "snarkvm-fields" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e38c91b1f2797726f3125c32e6837d15cbb473472ae4a6746a90388850fb361c" +checksum = "1a60e65f8e1c8be65456815cb016cf9c153f272948ebce50f01c5c7b9832eb5c" dependencies = [ "aleo-std", "anyhow", - "derivative", "itertools", "num-traits", - "rand", + "rand 0.10.2", "rayon", "serde", "snarkvm-utilities", - "thiserror", + "thiserror 2.0.18", "zeroize", ] [[package]] name = "snarkvm-ledger" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4aa5229794ceaca81f60f92135cdeb96ec694f15d7c643b9ab8639986e5747" +checksum = "b7d090a5d10c85218c8f302b7971ac26266fcf395ec60a7946d6be202585314e" dependencies = [ "aleo-std", "anyhow", "indexmap", + "lru", "parking_lot", - "rand", + "rand 0.10.2", + "rand_chacha 0.10.0", "rayon", "snarkvm-console", "snarkvm-ledger-authority", "snarkvm-ledger-block", - "snarkvm-ledger-coinbase", "snarkvm-ledger-committee", "snarkvm-ledger-narwhal", + "snarkvm-ledger-puzzle", "snarkvm-ledger-query", "snarkvm-ledger-store", "snarkvm-synthesizer", + "snarkvm-synthesizer-error", + "snarkvm-utilities", + "thiserror 2.0.18", "time", "tracing", ] [[package]] name = "snarkvm-ledger-authority" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebbd1fd021ff44bcd2921676cf2e5e1d09649f3c135f49c5556c5f2d57dc85c1" +checksum = "ca088fc9fbab4f64484f016c7b8f43ac7864e38962776ba5fe87e340b8a9d1d0" dependencies = [ "anyhow", - "rand", + "rand 0.10.2", "serde_json", "snarkvm-console", "snarkvm-ledger-narwhal-subdag", @@ -2335,60 +2859,47 @@ dependencies = [ [[package]] name = "snarkvm-ledger-block" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0b34daa6e379ff532ec59ebe7526334ae27b5be44486c88eb986db75e6212d" +checksum = "0ab7feccfa232a8125d5d08523f1097c2ff9f940df1b5dd680db33e22051b282" dependencies = [ + "anyhow", "indexmap", + "itertools", "rayon", "serde_json", "snarkvm-console", "snarkvm-ledger-authority", - "snarkvm-ledger-coinbase", "snarkvm-ledger-committee", + "snarkvm-ledger-narwhal-batch-header", + "snarkvm-ledger-narwhal-data", "snarkvm-ledger-narwhal-subdag", "snarkvm-ledger-narwhal-transmission-id", + "snarkvm-ledger-puzzle", + "snarkvm-synthesizer-error", "snarkvm-synthesizer-program", "snarkvm-synthesizer-snark", -] - -[[package]] -name = "snarkvm-ledger-coinbase" -version = "0.16.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdacdf3079c775cd6c9b4f8ce77a374e6bf96884ff0d060432a15755d37521f7" -dependencies = [ - "aleo-std", - "anyhow", - "bincode", - "blake2", - "indexmap", - "rayon", - "serde_json", - "snarkvm-algorithms", - "snarkvm-console", - "snarkvm-curves", - "snarkvm-fields", - "snarkvm-synthesizer-snark", "snarkvm-utilities", ] [[package]] name = "snarkvm-ledger-committee" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c085d059898e363ff3bbfa4541c3ce4bb803d9e019a1b153dbedc250909db" +checksum = "d71b4eb3a3f68f644f3a97badd97231a6142be4c544a827d33527de8bdf0a63b" dependencies = [ "indexmap", + "rayon", "serde_json", "snarkvm-console", + "snarkvm-ledger-narwhal-batch-header", ] [[package]] name = "snarkvm-ledger-narwhal" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d31641f8a147086f1c1327d7677c6cb578cf5d9c6347a6550c1bf19b021102b4" +checksum = "2e1fec476440f0ab21e9f4cb7de4ca0599e15fd96f4e2c36e5cde5cdeb0fd05c" dependencies = [ "snarkvm-ledger-narwhal-batch-certificate", "snarkvm-ledger-narwhal-batch-header", @@ -2400,11 +2911,12 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-certificate" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c273f32c68ad76ca284d9e7094bd2ea0a09ee7925088be05080fa849925b436d" +checksum = "2820939e81acdb3a1e956c02a1519a697c04c42500369e13334175c1be5a474c" dependencies = [ "indexmap", + "rayon", "serde_json", "snarkvm-console", "snarkvm-ledger-narwhal-batch-header", @@ -2413,11 +2925,12 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-header" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f7ad72bc7f32cec469af51eb82a7cb5d23d00cfd5c1bf39d3eca0f0197e5e8" +checksum = "fdc1aa3d423f36e151fded47358648b3e9f5bdef8f14b649867b2970a8b478f0" dependencies = [ "indexmap", + "rayon", "serde_json", "snarkvm-console", "snarkvm-ledger-narwhal-transmission-id", @@ -2425,9 +2938,9 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-data" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b562936230c19c30645ec8811a2e482172bd1ed682cc57f26571f7f7e2341f91" +checksum = "06c221e93a87d8d95d04a7ae80cbc002f6f234de6b0aeb2e4cc671472c4ed86a" dependencies = [ "bytes", "serde_json", @@ -2437,14 +2950,15 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-subdag" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece4480b32a23505cd1ed5d133c0dcf1473e7bbafce1ebf93cf0acab324a1f75" +checksum = "909c86ab8ffb66d68e65008a2bc7587f804f90c880e546959e012768f2f52c8a" dependencies = [ "indexmap", "rayon", "serde_json", "snarkvm-console", + "snarkvm-ledger-committee", "snarkvm-ledger-narwhal-batch-certificate", "snarkvm-ledger-narwhal-batch-header", "snarkvm-ledger-narwhal-transmission-id", @@ -2452,37 +2966,85 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b74493ad09983047da41842f6cc99202aaf51373f8829138b00440b4274f135" +checksum = "842e016abadbac845bcd8a07ed7ac5838aa770017c0fc0cc48f69e21b3b9b21b" dependencies = [ "bytes", "serde_json", "snarkvm-console", "snarkvm-ledger-block", - "snarkvm-ledger-coinbase", "snarkvm-ledger-narwhal-data", + "snarkvm-ledger-puzzle", ] [[package]] name = "snarkvm-ledger-narwhal-transmission-id" -version = "0.16.12" +version = "4.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90735fbb67ea6a28835fa4aca7fbd23f8082254001a26458fd54bd8e25a280ec" +dependencies = [ + "snarkvm-console", + "snarkvm-ledger-puzzle", +] + +[[package]] +name = "snarkvm-ledger-puzzle" +version = "4.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9930f2b8f6d7af1c4df5ca9e1fc58a1245614d602a23dcacdba2f49c196dce" +dependencies = [ + "aleo-std", + "anyhow", + "bincode", + "indexmap", + "lru", + "parking_lot", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rayon", + "serde_json", + "snarkvm-algorithms", + "snarkvm-console", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-ledger-puzzle-epoch" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be13d324fc359c1528d541cf50349d3dcc40f1caf461672c2e8a4927a0eff6dc" +checksum = "39769888aa99f476ba96f58f130baec1adf8cc2cb403e582f6b750d9fe7b4618" dependencies = [ + "aleo-std", + "anyhow", + "colored", + "indexmap", + "lru", + "parking_lot", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rayon", + "snarkvm-circuit", "snarkvm-console", - "snarkvm-ledger-coinbase", + "snarkvm-ledger-puzzle", + "snarkvm-synthesizer-process", + "snarkvm-synthesizer-program", + "snarkvm-utilities", ] [[package]] name = "snarkvm-ledger-query" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b91cc603f6656ff0b5a5456c6ba4daf3e6d5aa4d177dfdf1528c68161109c72" +checksum = "3fab416cca1cc120da5e23da9f96c33e63a66291d5b57900edff4e066eb37860" dependencies = [ + "anyhow", "async-trait", "reqwest", + "serde", + "serde_json", "snarkvm-console", + "snarkvm-ledger-block", "snarkvm-ledger-store", "snarkvm-synthesizer-program", "ureq", @@ -2490,96 +3052,123 @@ dependencies = [ [[package]] name = "snarkvm-ledger-store" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01541d583578d0ac73d9e30d4299796e8c770dd0686853ed48b7044ae71392bf" +checksum = "af053ce8e70479bf733f1060d8583f5508fe395afe1776494428dd8bd3d6b198" dependencies = [ + "aleo-std-storage", "anyhow", "bincode", "indexmap", "parking_lot", "rayon", "serde", + "serde_json", "snarkvm-console", "snarkvm-ledger-authority", "snarkvm-ledger-block", - "snarkvm-ledger-coinbase", "snarkvm-ledger-committee", "snarkvm-ledger-narwhal-batch-certificate", + "snarkvm-ledger-puzzle", "snarkvm-synthesizer-program", "snarkvm-synthesizer-snark", + "snarkvm-utilities", + "tracing", ] [[package]] name = "snarkvm-parameters" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2baaea9dea656ae00ad6411cc7640b192e3193061e7132d7c41dbb3460c62c4" +checksum = "ce50be3cbc636838eda13c6312fe1c957b3d09a61fe8d1636f2ce227be756e60" dependencies = [ "aleo-std", "anyhow", - "bincode", "cfg-if", "colored", - "curl", "hex", - "indexmap", - "itertools", "lazy_static", + "parking_lot", "paste", - "rand", + "rand 0.10.2", "serde_json", "sha2", "snarkvm-curves", "snarkvm-utilities", - "thiserror", + "thiserror 2.0.18", + "ureq", ] [[package]] name = "snarkvm-synthesizer" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048cea58b5917b36f7cb1c2c5bce6447248eeeee148105af8c8b812df8e3153c" +checksum = "5ef3f5387331260f4af6c6ce339814516e5783f6c78ec83cceb5a922688e25c7" dependencies = [ "aleo-std", "anyhow", "indexmap", + "itertools", + "lru", "parking_lot", - "rand", + "rand 0.10.2", "rayon", + "serde_json", "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", "snarkvm-ledger-block", - "snarkvm-ledger-coinbase", "snarkvm-ledger-committee", + "snarkvm-ledger-narwhal-data", + "snarkvm-ledger-puzzle", + "snarkvm-ledger-puzzle-epoch", "snarkvm-ledger-query", "snarkvm-ledger-store", + "snarkvm-synthesizer-error", "snarkvm-synthesizer-process", "snarkvm-synthesizer-program", "snarkvm-synthesizer-snark", + "snarkvm-utilities", + "tokio", "tracing", + "tracing-subscriber", +] + +[[package]] +name = "snarkvm-synthesizer-error" +version = "4.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f292aa7ab9d8db2a092256cf79ee704445b82a0bfdc008ddce2fde78bf5f56" +dependencies = [ + "anyhow", + "snarkvm-circuit-environment", + "snarkvm-console-network", + "snarkvm-console-program", + "thiserror 2.0.18", ] [[package]] name = "snarkvm-synthesizer-process" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a916d416fade91351bd296b9e6015c2eb4481302d045ba29b6ef927aa2278d6" +checksum = "c6e9a83a46498378477aaee8e7a07a5ddd2cd5a7c05bf9b2a1497cda317ac3dc" dependencies = [ "aleo-std", "colored", "indexmap", - "once_cell", + "itertools", "parking_lot", - "rand", + "rand 0.10.2", + "rand_chacha 0.10.0", "rayon", "serde_json", + "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", "snarkvm-ledger-block", "snarkvm-ledger-query", "snarkvm-ledger-store", + "snarkvm-synthesizer-error", "snarkvm-synthesizer-program", "snarkvm-synthesizer-snark", "snarkvm-utilities", @@ -2587,84 +3176,93 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-program" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32f15bb9d2526bbae866d458bd527bf0394e450d9ccf0ad3d4ba55c3bc2a78" +checksum = "c22fa0ec46f528d531af6ab72005a51acff64d7c9a9732384f11c59c65f64f50" dependencies = [ + "enum-iterator", "indexmap", "paste", - "rand", - "rand_chacha", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rayon", "serde_json", + "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", + "snarkvm-synthesizer-error", + "snarkvm-synthesizer-snark", + "snarkvm-utilities", + "tiny-keccak", ] [[package]] name = "snarkvm-synthesizer-snark" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b8879dfdd0472dc3b369accf4e4589b29556f2917fd35e9cb6f98d5160023e0" +checksum = "ef07277ee50c309a3e74d49d869ffc5e4e358a00b57738760cdb2a8e2ab77ebe" dependencies = [ "bincode", - "once_cell", "serde_json", "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", + "snarkvm-utilities", ] [[package]] name = "snarkvm-utilities" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90fb901c04e68974e1006f4c77126b2f7a65a1e645589aec1b5d4aab0bdde49" +checksum = "eb4d04f7357366dcbbae21249334ed2ad5ffefc09bd00f0d418f4bba34fba86c" dependencies = [ "aleo-std", "anyhow", "bincode", + "colored", "num-bigint", "num_cpus", - "rand", + "rand 0.10.2", "rand_xorshift", "rayon", "serde", "serde_json", "smol_str", "snarkvm-utilities-derives", - "thiserror", + "thiserror 2.0.18", + "tracing", "zeroize", ] [[package]] name = "snarkvm-utilities-derives" -version = "0.16.12" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3ef65861467314b189dcb824587d13fde96e1b39b88d9b72440e15fb7d25d" +checksum = "7f50ca261adb518781bf4cfb9d6b69c385d00fca93bf5b5e6838c93598206bd8" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] name = "socket2" -version = "0.4.10" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "socket2" -version = "0.5.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -2674,16 +3272,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "strsim" -version = "0.10.0" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2703,49 +3301,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.46", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.39" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.46", "unicode-ident", ] [[package]] -name = "synom" -version = "0.11.3" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "unicode-xid", + "futures-core", ] [[package]] -name = "system-configuration" -version = "0.5.1" +name = "synom" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", + "unicode-xid", ] [[package]] -name = "system-configuration-sys" -version = "0.5.0" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "core-foundation-sys", - "libc", + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -2756,15 +3353,15 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", - "fastrand 2.0.1", - "redox_syscall", + "fastrand", + "getrandom 0.4.3", + "once_cell", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2773,7 +3370,16 @@ version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.50", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2783,27 +3389,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", ] [[package]] name = "time" -version = "0.3.30" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", + "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", + "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] [[package]] name = "tiny-keccak" @@ -2814,6 +3452,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2839,7 +3487,6 @@ dependencies = [ "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2 0.5.5", "windows-sys 0.48.0", @@ -2856,24 +3503,59 @@ dependencies = [ ] [[package]] -name = "tokio-util" -version = "0.7.10" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "bytes", "futures-core", - "futures-sink", + "futures-util", "pin-project-lite", + "sync_wrapper", "tokio", - "tracing", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", ] +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -2893,17 +3575,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ + "log", "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2914,9 +3622,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-bidi" @@ -2939,12 +3647,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - [[package]] name = "unicode-xid" version = "0.0.4" @@ -2965,22 +3667,39 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64", + "cookie_store", + "der", "flate2", "log", - "once_cell", + "native-tls", + "percent-encoding", "rustls", - "rustls-webpki", + "rustls-pki-types", "serde", "serde_json", - "url", + "ureq-proto", + "utf8-zero", + "webpki-root-certs", "webpki-roots", ] +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.0" @@ -2988,21 +3707,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] [[package]] -name = "urlencoding" -version = "2.1.3" +name = "utf8-zero" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" [[package]] -name = "utf8parse" -version = "0.2.1" +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -3043,26 +3768,14 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" -dependencies = [ - "bumpalo", - "log", "once_cell", - "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -3080,32 +3793,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ - "quote 1.0.33", + "quote 1.0.46", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", - "wasm-bindgen-backend", + "quote 1.0.46", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" @@ -3117,11 +3833,33 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" -version = "0.25.3" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "winapi" @@ -3155,13 +3893,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -3182,18 +3917,12 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-link", ] [[package]] @@ -3226,12 +3955,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3244,12 +3967,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3262,12 +3979,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3280,12 +3991,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3298,12 +4003,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3316,12 +4015,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3334,12 +4027,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3353,51 +4040,121 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] -name = "winreg" -version = "0.50.0" +name = "wnaf" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "ff", + "group", + "hybrid-array", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", + "synstructure", ] [[package]] -name = "zerocopy" -version = "0.7.32" +name = "zerofrom" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ - "zerocopy-derive", + "zerofrom-derive", ] [[package]] -name = "zerocopy-derive" -version = "0.7.32" +name = "zerofrom-derive" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", + "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.39", + "quote 1.0.46", + "syn 2.0.118", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index e424d74..b76272e 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -10,6 +10,11 @@ license = "GPL-3.0-or-later" name = "_aleolib" crate-type = ["cdylib"] +[features] +default = ["mainnet"] +mainnet = [] +testnet = [] + [dependencies] anyhow = "1" indexmap = "2.1.0" @@ -20,4 +25,6 @@ rand = { version = "^0.8" } serde = "1" serde_json = "1" -snarkvm = { version = "0.16.6", features = ["console"] } +snarkvm = { version = "4.7.3", default-features = false, features = [ + "console", "circuit", "synthesizer", "ledger", "utilities", "algorithms", "parameters", +] } From 21d3db63720c575223c3eac8cefe4f38047e299c Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 10:18:49 -0500 Subject: [PATCH 02/72] feat: drop coinbase/puzzle module (removed in snarkvm 4.x) --- sdk/src/coinbase/epoch_challenge.rs | 117 ---------------------------- sdk/src/coinbase/mod.rs | 27 ------- sdk/src/coinbase/prover_solution.rs | 88 --------------------- sdk/src/coinbase/puzzle.rs | 40 ---------- sdk/src/coinbase/verifying_key.rs | 42 ---------- 5 files changed, 314 deletions(-) delete mode 100644 sdk/src/coinbase/epoch_challenge.rs delete mode 100644 sdk/src/coinbase/mod.rs delete mode 100644 sdk/src/coinbase/prover_solution.rs delete mode 100644 sdk/src/coinbase/puzzle.rs delete mode 100644 sdk/src/coinbase/verifying_key.rs diff --git a/sdk/src/coinbase/epoch_challenge.rs b/sdk/src/coinbase/epoch_challenge.rs deleted file mode 100644 index 0bf8a24..0000000 --- a/sdk/src/coinbase/epoch_challenge.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (C) 2019-2023 Aleo Systems 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 . - -use crate::types::EpochChallengeNative; - -use pyo3::prelude::*; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use snarkvm::prelude::{FromBytes, ToBytes}; - -use std::ops::Deref; - -/// The Aleo coinbase epoch challenge type. -#[pyclass(frozen)] -pub struct EpochChallenge(EpochChallengeNative); - -impl Serialize for EpochChallenge { - /// Serializes the solutions to a JSON-string or buffer. - fn serialize(&self, serializer: S) -> Result { - use serde::ser::SerializeStruct; - use snarkvm::prelude::ToBytesSerializer; - match serializer.is_human_readable() { - true => { - let mut s = serializer.serialize_struct("EpochChallenge", 1)?; - s.serialize_field("epoch_number", &self.0.epoch_number())?; - s.serialize_field("epoch_block_hash", &self.0.epoch_block_hash())?; - s.serialize_field("degree", &self.0.degree())?; - s.end() - } - false => ToBytesSerializer::serialize_with_size_encoding(&self.0, serializer), - } - } -} - -impl<'de> Deserialize<'de> for EpochChallenge { - /// Deserializes the partial solution from a JSON-string or buffer. - fn deserialize>(deserializer: D) -> Result { - use snarkvm::prelude::FromBytesDeserializer; - use snarkvm::utilities::DeserializeExt; - - let native = match deserializer.is_human_readable() { - true => { - let mut v = serde_json::Value::deserialize(deserializer)?; - EpochChallengeNative::new( - DeserializeExt::take_from_value::(&mut v, "epoch_number")?, - DeserializeExt::take_from_value::(&mut v, "epoch_block_hash")?, - DeserializeExt::take_from_value::(&mut v, "degree")?, - ) - .map_err(|e| { - serde::de::Error::custom(format!("Could not create EpochChallenge: {e}")) - })? - } - false => FromBytesDeserializer::::deserialize_with_size_encoding( - deserializer, - "epoch challenge", - )?, - }; - Ok(Self(native)) - } -} - -impl Deref for EpochChallenge { - type Target = EpochChallengeNative; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[pymethods] -impl EpochChallenge { - /// Parses an EpochChallenge from a JSON string. - #[staticmethod] - fn from_json(json: &str) -> anyhow::Result { - Ok(serde_json::from_str(json)?) - } - - /// Serialize the given EpochChallenge as a JSON string. - fn to_json(&self) -> anyhow::Result { - Ok(serde_json::to_string(&self)?) - } - - /// Constructs an EpochChallenge from a byte array. - #[staticmethod] - fn from_bytes(bytes: &[u8]) -> anyhow::Result { - EpochChallengeNative::from_bytes_le(bytes).map(Self) - } - - /// Returns the byte representation of an EpochChallenge. - fn bytes(&self) -> anyhow::Result> { - self.0.to_bytes_le() - } - - /// Returns the EpochChallenge as a JSON string. - fn __str__(&self) -> anyhow::Result { - self.to_json() - } - - fn __eq__(&self, other: &Self) -> bool { - self.0 == other.0 - } - - #[classattr] - const __hash__: Option = None; -} diff --git a/sdk/src/coinbase/mod.rs b/sdk/src/coinbase/mod.rs deleted file mode 100644 index 22111ee..0000000 --- a/sdk/src/coinbase/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2019-2023 Aleo Systems 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 . - -mod epoch_challenge; -pub use epoch_challenge::EpochChallenge; - -mod prover_solution; -pub use prover_solution::ProverSolution; - -mod puzzle; -pub use puzzle::CoinbasePuzzle; - -mod verifying_key; -pub use verifying_key::CoinbaseVerifyingKey; diff --git a/sdk/src/coinbase/prover_solution.rs b/sdk/src/coinbase/prover_solution.rs deleted file mode 100644 index ac90034..0000000 --- a/sdk/src/coinbase/prover_solution.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (C) 2019-2023 Aleo Systems 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 . - -use crate::{ - account::Address, - coinbase::{CoinbaseVerifyingKey, EpochChallenge}, - types::ProverSolutionNative, -}; - -use pyo3::prelude::*; -use snarkvm::prelude::{FromBytes, ToBytes}; - -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; - -/// The prover solution for the coinbase puzzle from a prover. -#[pyclass(frozen)] -pub struct ProverSolution(ProverSolutionNative); - -#[pymethods] -impl ProverSolution { - /// Reads in a ProverSolution from a JSON string. - #[staticmethod] - fn from_json(json: &str) -> anyhow::Result { - Ok(Self(serde_json::from_str(json)?)) - } - - /// Serialize the given ProverSolution as a JSON string. - fn to_json(&self) -> anyhow::Result { - Ok(serde_json::to_string(&self.0)?) - } - - /// Constructs a ProverSolution from a byte array. - #[staticmethod] - fn from_bytes(bytes: &[u8]) -> anyhow::Result { - ProverSolutionNative::from_bytes_le(bytes).map(Self) - } - - /// Returns the byte representation of a ProverSolution. - fn bytes(&self) -> anyhow::Result> { - self.0.to_bytes_le() - } - - /// Returns the address of the prover. - fn address(&self) -> Address { - Address::from(self.0.address()) - } - - /// Returns `true` if the prover solution is valid. - fn verify( - &self, - verifying_key: &CoinbaseVerifyingKey, - epoch_challenge: &EpochChallenge, - proof_target: u64, - ) -> anyhow::Result { - self.0.verify(verifying_key, epoch_challenge, proof_target) - } - - /// Returns the ProverSolution as a JSON string. - fn __str__(&self) -> anyhow::Result { - self.to_json() - } - - fn __eq__(&self, other: &Self) -> bool { - self.0 == other.0 - } - - fn __hash__(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.0.hash(&mut hasher); - hasher.finish() - } -} diff --git a/sdk/src/coinbase/puzzle.rs b/sdk/src/coinbase/puzzle.rs deleted file mode 100644 index f1656e2..0000000 --- a/sdk/src/coinbase/puzzle.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2019-2023 Aleo Systems 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 . - -use crate::{coinbase::CoinbaseVerifyingKey, types::CoinbasePuzzleNative}; - -use pyo3::prelude::*; - -/// The Aleo coinbase puzzle type. -#[pyclass] -pub struct CoinbasePuzzle(CoinbasePuzzleNative); - -#[pymethods] -impl CoinbasePuzzle { - /// Load the coinbase puzzle proving and verifying keys. - #[staticmethod] - fn load() -> anyhow::Result { - CoinbasePuzzleNative::load().map(Self) - } - - /// Returns the coinbase verifying key. - fn verifying_key(&self) -> CoinbaseVerifyingKey { - self.0.coinbase_verifying_key().clone().into() - } - - #[classattr] - const __hash__: Option = None; -} diff --git a/sdk/src/coinbase/verifying_key.rs b/sdk/src/coinbase/verifying_key.rs deleted file mode 100644 index bf4686d..0000000 --- a/sdk/src/coinbase/verifying_key.rs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2019-2023 Aleo Systems 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 . - -use crate::types::CoinbaseVerifyingKeyNative; - -use pyo3::prelude::*; - -use std::ops::Deref; - -/// The Aleo coinbase verifying key type. -#[pyclass(frozen)] -pub struct CoinbaseVerifyingKey(CoinbaseVerifyingKeyNative); - -#[pymethods] -impl CoinbaseVerifyingKey {} - -impl From for CoinbaseVerifyingKey { - fn from(value: CoinbaseVerifyingKeyNative) -> Self { - Self(value) - } -} - -impl Deref for CoinbaseVerifyingKey { - type Target = CoinbaseVerifyingKeyNative; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} From e515bb551e13b03a58d0b47103d2ae339404d5cb Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 10:24:55 -0500 Subject: [PATCH 03/72] feat: cfg-select network in types.rs, migrate aliases to snarkvm 4.7.3 paths Replace hardcoded Testnet3 with cfg-selected MainnetV0/TestnetV0 network types. Update imports to use 4.7.3 API paths: ledger::{block, query, store}, synthesizer for Process and Trace. Remove deprecated coinbase module imports. Add feature-gated AleoTestnetV0 import. Co-Authored-By: Claude Fable 5 --- sdk/src/types.rs | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/sdk/src/types.rs b/sdk/src/types.rs index 20e8432..6013b90 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -15,20 +15,30 @@ // along with the Aleo SDK library. If not, see . use snarkvm::circuit::network::AleoV0; -use snarkvm::console::network::Testnet3; -use snarkvm::prelude::coinbase::{ - CoinbasePuzzle, CoinbaseVerifyingKey, EpochChallenge, ProverSolution, -}; -use snarkvm::prelude::query::Query; -use snarkvm::prelude::store::helpers::memory::BlockMemory; -use snarkvm::prelude::transaction::Transaction; +#[cfg(feature = "testnet")] +use snarkvm::circuit::network::AleoTestnetV0; +use snarkvm::console::network::{MainnetV0, TestnetV0}; +use snarkvm::ledger::block::{Fee, Transaction}; +use snarkvm::ledger::query::Query; +use snarkvm::ledger::store::helpers::memory::BlockMemory; +use snarkvm::synthesizer::{Process, Trace}; use snarkvm::prelude::{ - Address, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Fee, Field, Group, - Identifier, Literal, Locator, Plaintext, PrivateKey, Process, Program, ProgramID, ProvingKey, - Record, Response, Scalar, Signature, Trace, Transition, Value, VerifyingKey, ViewKey, I128, + Address, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, Group, + Identifier, Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, ProvingKey, + Record, Response, Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, I128, I16, I32, I64, I8, U128, U16, U32, U64, U8, }; +// Network selection (feature-gated; MainnetV0 by default). +#[cfg(not(feature = "testnet"))] +pub type CurrentNetwork = MainnetV0; +#[cfg(not(feature = "testnet"))] +pub type CurrentAleo = AleoV0; +#[cfg(feature = "testnet")] +pub type CurrentNetwork = TestnetV0; +#[cfg(feature = "testnet")] +pub type CurrentAleo = AleoTestnetV0; + // Account types pub type AddressNative = Address; pub type ComputeKeyNative = ComputeKey; @@ -54,12 +64,8 @@ pub type U32Native = U32; pub type U64Native = U64; pub type U128Native = U128; -// Network types -pub type CurrentNetwork = Testnet3; -pub type CurrentAleo = AleoV0; - -// Program Types -type CurrentBlockMemory = BlockMemory; +// Program types +pub type CurrentBlockMemory = BlockMemory; pub type AuthorizationNative = Authorization; pub type ExecutionNative = Execution; pub type FeeNative = Fee; @@ -83,9 +89,3 @@ pub type CiphertextNative = Ciphertext; pub type PlaintextNative = Plaintext; pub type RecordCiphertextNative = Record; pub type RecordPlaintextNative = Record; - -// Coinbase types -pub type CoinbasePuzzleNative = CoinbasePuzzle; -pub type CoinbaseVerifyingKeyNative = CoinbaseVerifyingKey; -pub type EpochChallengeNative = EpochChallenge; -pub type ProverSolutionNative = ProverSolution; From 9c286d70ff29fccc63d1d6ecaf1097faf0a2e62a Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 10:27:13 -0500 Subject: [PATCH 04/72] fix: cfg-gate network imports to keep both builds warning-free --- sdk/src/types.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/src/types.rs b/sdk/src/types.rs index 6013b90..cd6c2b8 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -14,10 +14,14 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see . +#[cfg(not(feature = "testnet"))] use snarkvm::circuit::network::AleoV0; #[cfg(feature = "testnet")] use snarkvm::circuit::network::AleoTestnetV0; -use snarkvm::console::network::{MainnetV0, TestnetV0}; +#[cfg(not(feature = "testnet"))] +use snarkvm::console::network::MainnetV0; +#[cfg(feature = "testnet")] +use snarkvm::console::network::TestnetV0; use snarkvm::ledger::block::{Fee, Transaction}; use snarkvm::ledger::query::Query; use snarkvm::ledger::store::helpers::memory::BlockMemory; From 12b4fb2edcf82f32d8b1e1758175169ea8346297 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 10:29:13 -0500 Subject: [PATCH 05/72] feat: two-build pymodule (_aleolib_mainnet/_testnet), drop coinbase classes Replaces single _aleolib pymodule with two cfg-gated variants: - _aleolib_mainnet (default, no testnet feature) - _aleolib_testnet (when testnet feature enabled) Both delegate to shared register() function. Removes coinbase module and drops CoinbasePuzzle, CoinbaseVerifyingKey, EpochChallenge, and ProverSolution class registrations. Co-Authored-By: Claude Fable 5 --- sdk/src/lib.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 8a31937..293e7cb 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -18,7 +18,6 @@ use pyo3::prelude::*; mod account; mod algebra; -mod coinbase; mod credits; mod network; mod programs; @@ -26,7 +25,6 @@ mod types; use account::*; use algebra::*; -use coinbase::*; use credits::*; use network::*; use programs::*; @@ -34,19 +32,15 @@ use programs::*; /// The Aleo Python SDK provides a set of libraries aimed at empowering /// Python developers with zk (zero-knowledge) programming capabilities /// via the usage of Aleo's zkSnarks. -#[pymodule] -#[pyo3(name = "_aleolib")] -fn register_module(_py: Python, m: &PyModule) -> PyResult<()> { +/// Registers all classes into the module (shared by both network builds). +fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::
()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -66,7 +60,6 @@ fn register_module(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -87,3 +80,17 @@ fn register_module(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } + +#[cfg(not(feature = "testnet"))] +#[pymodule] +#[pyo3(name = "_aleolib_mainnet")] +fn aleolib_mainnet(py: Python, m: &PyModule) -> PyResult<()> { + register(py, m) +} + +#[cfg(feature = "testnet")] +#[pymodule] +#[pyo3(name = "_aleolib_testnet")] +fn aleolib_testnet(py: Python, m: &PyModule) -> PyResult<()> { + register(py, m) +} From b6aa37e519fc024c7eaf91c47c09fb66bd68ad40 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 11:02:32 -0500 Subject: [PATCH 06/72] feat: port algebra module to snarkvm 4.7.3 Replace rand 0.8 Standard distribution sampling with snarkvm 4.7.3's Uniform::rand API (rand 0.10). Upgrade sdk rand dep to 0.10 to align with snarkvm's transitive rand version. Co-Authored-By: Claude Fable 5 --- sdk/Cargo.lock | 98 ++++++++++++++-------------------------- sdk/Cargo.toml | 2 +- sdk/src/algebra/field.rs | 8 ++-- 3 files changed, 38 insertions(+), 70 deletions(-) diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index 4ad2368..6345d29 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -32,7 +32,7 @@ dependencies = [ "once_cell", "openssl", "pyo3", - "rand 0.8.5", + "rand", "serde", "serde_json", "snarkvm", @@ -327,7 +327,7 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -494,7 +494,7 @@ dependencies = [ "ctutils", "hybrid-array", "num-traits", - "rand_core 0.10.1", + "rand_core", "subtle", "zeroize", ] @@ -516,7 +516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -648,7 +648,7 @@ dependencies = [ "ff", "group", "hybrid-array", - "rand_core 0.10.1", + "rand_core", "sec1", "subtle", "zeroize", @@ -718,7 +718,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core 0.10.1", + "rand_core", "subtle", ] @@ -855,7 +855,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "rand_core 0.10.1", + "rand_core", "wasm-bindgen", ] @@ -872,7 +872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core 0.10.1", + "rand_core", "subtle", ] @@ -1657,7 +1657,7 @@ dependencies = [ "crypto-bigint", "crypto-common 0.2.2", "ff", - "rand_core 0.10.1", + "rand_core", "subtle", "zeroize", ] @@ -1774,7 +1774,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand 0.10.2", + "rand", "rand_pcg", "ring", "rustc-hash", @@ -1822,17 +1822,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.10.2" @@ -1841,17 +1830,7 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1861,16 +1840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" dependencies = [ "ppv-lite86", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.11", + "rand_core", ] [[package]] @@ -1885,7 +1855,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -1894,7 +1864,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" dependencies = [ - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -2268,7 +2238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ "digest 0.11.3", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -2324,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2051aaa63e873040259b906bfdf0e196b7fad78548bf117d50b0677fa9cc5f6" dependencies = [ "anyhow", - "rand 0.10.2", + "rand", "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", @@ -2351,7 +2321,7 @@ dependencies = [ "indexmap", "itertools", "num-traits", - "rand 0.10.2", + "rand", "rayon", "serde", "sha2", @@ -2656,7 +2626,7 @@ dependencies = [ "itertools", "nom", "num-traits", - "rand 0.10.2", + "rand", "serde", "snarkvm-curves", "snarkvm-fields", @@ -2788,7 +2758,7 @@ version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb85f50dc014ca37d78e91187e3ed6f2c5f36c505656841647c8a7d16d3951e" dependencies = [ - "rand 0.10.2", + "rand", "rustc_version", "serde", "snarkvm-fields", @@ -2806,7 +2776,7 @@ dependencies = [ "anyhow", "itertools", "num-traits", - "rand 0.10.2", + "rand", "rayon", "serde", "snarkvm-utilities", @@ -2825,8 +2795,8 @@ dependencies = [ "indexmap", "lru", "parking_lot", - "rand 0.10.2", - "rand_chacha 0.10.0", + "rand", + "rand_chacha", "rayon", "snarkvm-console", "snarkvm-ledger-authority", @@ -2851,7 +2821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca088fc9fbab4f64484f016c7b8f43ac7864e38962776ba5fe87e340b8a9d1d0" dependencies = [ "anyhow", - "rand 0.10.2", + "rand", "serde_json", "snarkvm-console", "snarkvm-ledger-narwhal-subdag", @@ -3000,8 +2970,8 @@ dependencies = [ "indexmap", "lru", "parking_lot", - "rand 0.10.2", - "rand_chacha 0.10.0", + "rand", + "rand_chacha", "rayon", "serde_json", "snarkvm-algorithms", @@ -3021,8 +2991,8 @@ dependencies = [ "indexmap", "lru", "parking_lot", - "rand 0.10.2", - "rand_chacha 0.10.0", + "rand", + "rand_chacha", "rayon", "snarkvm-circuit", "snarkvm-console", @@ -3090,7 +3060,7 @@ dependencies = [ "lazy_static", "parking_lot", "paste", - "rand 0.10.2", + "rand", "serde_json", "sha2", "snarkvm-curves", @@ -3111,7 +3081,7 @@ dependencies = [ "itertools", "lru", "parking_lot", - "rand 0.10.2", + "rand", "rayon", "serde_json", "snarkvm-algorithms", @@ -3158,8 +3128,8 @@ dependencies = [ "indexmap", "itertools", "parking_lot", - "rand 0.10.2", - "rand_chacha 0.10.0", + "rand", + "rand_chacha", "rayon", "serde_json", "snarkvm-algorithms", @@ -3183,8 +3153,8 @@ dependencies = [ "enum-iterator", "indexmap", "paste", - "rand 0.10.2", - "rand_chacha 0.10.0", + "rand", + "rand_chacha", "rayon", "serde_json", "snarkvm-algorithms", @@ -3222,7 +3192,7 @@ dependencies = [ "colored", "num-bigint", "num_cpus", - "rand 0.10.2", + "rand", "rand_xorshift", "rayon", "serde", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index b76272e..02aad35 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -21,7 +21,7 @@ indexmap = "2.1.0" once_cell = "1.18.0" openssl = "0.10" pyo3 = { version = "0.20.0", features = ["extension-module", "abi3-py37", "anyhow"] } -rand = { version = "^0.8" } +rand = { version = "0.10" } serde = "1" serde_json = "1" diff --git a/sdk/src/algebra/field.rs b/sdk/src/algebra/field.rs index 218ca87..a515caf 100644 --- a/sdk/src/algebra/field.rs +++ b/sdk/src/algebra/field.rs @@ -17,8 +17,8 @@ use crate::types::FieldNative; use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; -use rand::{distributions::Standard, prelude::*}; -use snarkvm::prelude::Zero; +use rand::rngs::StdRng; +use snarkvm::prelude::{Uniform, Zero}; use std::{ collections::hash_map::DefaultHasher, @@ -43,9 +43,7 @@ impl Field { /// Generates a new field using a cryptographically secure random number generator #[staticmethod] fn random() -> Self { - StdRng::from_entropy() - .sample::(Standard) - .into() + ::rand(&mut rand::make_rng::()).into() } /// Initializes a new field as a domain separator. From ba141032c6815956311b78c9e6c9f8a7c329d1d2 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 11:11:32 -0500 Subject: [PATCH 07/72] feat: port credits and network modules to snarkvm 4.7.3 The EDITION constant was removed in snarkvm 4.7.3. Replace with hardcoded 0 value. Credits module requires no changes (pure native types). Co-Authored-By: Claude Fable 5 --- sdk/src/network/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/network/mod.rs b/sdk/src/network/mod.rs index caa6a2c..0a81f5a 100644 --- a/sdk/src/network/mod.rs +++ b/sdk/src/network/mod.rs @@ -42,7 +42,7 @@ impl Network { /// The network version. #[staticmethod] fn edition() -> u16 { - CurrentNetwork::EDITION + 0 // Network edition is fixed to 0 in snarkvm 4.7.3 } /// Returns the Poseidon hash with an input rate of 2. From 5f228eff9ac2bee89c5937e1ed4f49e240cf1b0a Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 11:13:51 -0500 Subject: [PATCH 08/72] =?UTF-8?q?fix:=20drop=20Network.edition()=20?= =?UTF-8?q?=E2=80=94=20EDITION=20removed=20from=20snarkvm=204.x=20Network?= =?UTF-8?q?=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/src/network/mod.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sdk/src/network/mod.rs b/sdk/src/network/mod.rs index 0a81f5a..258b316 100644 --- a/sdk/src/network/mod.rs +++ b/sdk/src/network/mod.rs @@ -39,12 +39,6 @@ impl Network { CurrentNetwork::ID } - /// The network version. - #[staticmethod] - fn edition() -> u16 { - 0 // Network edition is fixed to 0 in snarkvm 4.7.3 - } - /// Returns the Poseidon hash with an input rate of 2. #[staticmethod] fn hash_psd2(input: Vec) -> anyhow::Result { From 79fbac82edc6e21e0349bd12f1b17763d2e2b2ad Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 8 Jul 2026 11:18:53 -0500 Subject: [PATCH 09/72] feat: port account module to snarkvm 4.7.3 (record version, property getters, random()) - Replace StdRng::from_entropy() with rand::make_rng::() (rand 0.10) - Rename #[new] fn new() -> #[staticmethod] fn random() on PrivateKey and Account - Add Record::version() getter (U8 deref'd to machine u8) - Add #[getter] to pure accessors: PrivateKey::{address,view_key,compute_key,seed}, Signature::{challenge,response,compute_key}, ViewKey::to_address, ComputeKey::address, Account::{private_key,view_key,address}, RecordPlaintext::{owner,nonce} - Fix Plaintext::Struct/Array variant to use std::sync::OnceLock (was OnceCell) - Update serial_number() to accept record_view_key for new to_commitment() 3-arg signature Co-Authored-By: Claude Fable 5 --- sdk/src/account/compute_key.rs | 1 + sdk/src/account/mod.rs | 9 ++++++--- sdk/src/account/private_key.rs | 13 ++++++++----- sdk/src/account/record.rs | 11 ++++++++++- sdk/src/account/signature.rs | 7 +++++-- sdk/src/account/text.rs | 5 ++--- sdk/src/account/view_key.rs | 1 + 7 files changed, 33 insertions(+), 14 deletions(-) diff --git a/sdk/src/account/compute_key.rs b/sdk/src/account/compute_key.rs index 03f1908..593b398 100644 --- a/sdk/src/account/compute_key.rs +++ b/sdk/src/account/compute_key.rs @@ -27,6 +27,7 @@ pub struct ComputeKey(ComputeKeyNative); #[pymethods] impl ComputeKey { /// Returns the address from the compute key. + #[getter] fn address(&self) -> Address { self.0.to_address().into() } diff --git a/sdk/src/account/mod.rs b/sdk/src/account/mod.rs index 63d2718..39d3a12 100644 --- a/sdk/src/account/mod.rs +++ b/sdk/src/account/mod.rs @@ -55,9 +55,9 @@ pub struct Account { #[pymethods] impl Account { /// Generates a new account using a cryptographically secure random number generator - #[new] - fn new() -> Self { - Self::from(PrivateKey::new()) + #[staticmethod] + fn random() -> Self { + Self::from(PrivateKey::random()) } /// Creates a new account from the given private key. @@ -73,16 +73,19 @@ impl Account { } /// Returns an account private key. + #[getter] fn private_key(&self) -> PrivateKey { self.private_key } /// Returns an account view key. + #[getter] fn view_key(&self) -> ViewKey { self.view_key.clone() } /// Returns an account address. + #[getter] fn address(&self) -> Address { self.address.clone() } diff --git a/sdk/src/account/private_key.rs b/sdk/src/account/private_key.rs index 697f002..f6963bd 100644 --- a/sdk/src/account/private_key.rs +++ b/sdk/src/account/private_key.rs @@ -20,7 +20,7 @@ use crate::{ }; use pyo3::prelude::*; -use rand::{rngs::StdRng, SeedableRng}; +use rand::rngs::StdRng; use std::{ collections::hash_map::DefaultHasher, @@ -37,20 +37,21 @@ pub struct PrivateKey(PrivateKeyNative); #[pymethods] impl PrivateKey { /// Generates a new private key using a cryptographically secure random number generator - #[allow(clippy::new_without_default)] - #[new] - pub fn new() -> Self { - PrivateKeyNative::new(&mut StdRng::from_entropy()) + #[staticmethod] + pub fn random() -> Self { + PrivateKeyNative::new(&mut rand::make_rng::()) .unwrap() .into() } /// Derives the account address from an account private key. + #[getter] pub fn address(&self) -> anyhow::Result
{ AddressNative::try_from(&self.0).map(Into::into) } /// Derives the account compute key from an account private key. + #[getter] fn compute_key(&self) -> ComputeKey { ComputeKeyNative::try_from(&self.0).unwrap().into() } @@ -68,6 +69,7 @@ impl PrivateKey { } /// Returns the account seed. + #[getter] fn seed(&self) -> Field { self.0.seed().into() } @@ -88,6 +90,7 @@ impl PrivateKey { } /// Initializes a new account view key from an account private key. + #[getter] pub fn view_key(&self) -> ViewKey { let view_key = ViewKeyNative::try_from(&self.0).unwrap(); view_key.into() diff --git a/sdk/src/account/record.rs b/sdk/src/account/record.rs index 3297600..074a524 100644 --- a/sdk/src/account/record.rs +++ b/sdk/src/account/record.rs @@ -79,12 +79,20 @@ impl RecordPlaintext { RecordPlaintextNative::from_str(s).map(Self) } + /// Returns the version of the program record. + #[getter] + fn version(&self) -> u8 { + **self.0.version() + } + /// Returns the owner of the record as a string + #[getter] fn owner(&self) -> String { self.0.owner().to_string() } /// Returns the nonce of the program record. + #[getter] fn nonce(&self) -> Group { (*self.0.nonce()).into() } @@ -95,8 +103,9 @@ impl RecordPlaintext { private_key: &PrivateKey, program_id: &ProgramID, record_identifier: &Identifier, + record_view_key: &Field, ) -> anyhow::Result { - let commitment = self.to_commitment(program_id, record_identifier)?; + let commitment = self.to_commitment(program_id, record_identifier, &**record_view_key)?; RecordPlaintextNative::serial_number(**private_key, commitment).map(Into::into) } diff --git a/sdk/src/account/signature.rs b/sdk/src/account/signature.rs index 5461415..18a00dd 100644 --- a/sdk/src/account/signature.rs +++ b/sdk/src/account/signature.rs @@ -17,7 +17,7 @@ use crate::{types::SignatureNative, Address, ComputeKey, PrivateKey, Scalar}; use pyo3::prelude::*; -use rand::{rngs::StdRng, SeedableRng}; +use rand::rngs::StdRng; use std::{ collections::hash_map::DefaultHasher, @@ -34,11 +34,13 @@ pub struct Signature(SignatureNative); #[pymethods] impl Signature { /// Returns the verifier challenge. + #[getter] fn challenge(&self) -> Scalar { self.0.challenge().into() } /// Returns the signer compute key. + #[getter] fn compute_key(&self) -> ComputeKey { self.0.compute_key().into() } @@ -50,6 +52,7 @@ impl Signature { } /// Returns the prover response. + #[getter] fn response(&self) -> Scalar { self.0.response().into() } @@ -58,7 +61,7 @@ impl Signature { #[staticmethod] pub fn sign(private_key: &PrivateKey, message: &[u8]) -> anyhow::Result { private_key - .sign_bytes(message, &mut StdRng::from_entropy()) + .sign_bytes(message, &mut rand::make_rng::()) .map(Self) } diff --git a/sdk/src/account/text.rs b/sdk/src/account/text.rs index f4e2f3c..c1cfc83 100644 --- a/sdk/src/account/text.rs +++ b/sdk/src/account/text.rs @@ -20,7 +20,6 @@ use crate::{ }; use std::ops::Deref; -use once_cell::sync::OnceCell; use pyo3::{exceptions::PyTypeError, prelude::*}; use std::{collections::HashMap, str::FromStr}; @@ -98,14 +97,14 @@ impl Plaintext { #[staticmethod] fn new_struct(kv: Vec<(Identifier, Plaintext)>) -> Self { let kv: Vec<_> = kv.into_iter().map(|(k, v)| (k.into(), v.into())).collect(); - PlaintextNative::Struct(indexmap::IndexMap::from_iter(kv), OnceCell::new()).into() + PlaintextNative::Struct(indexmap::IndexMap::from_iter(kv), std::sync::OnceLock::new()).into() } /// Returns a new Plaintext::Array from a list of values. #[staticmethod] fn new_array(values: Vec) -> Self { let values: Vec<_> = values.into_iter().map(Into::into).collect(); - PlaintextNative::Array(values, OnceCell::new()).into() + PlaintextNative::Array(values, std::sync::OnceLock::new()).into() } /// Encrypts self to the given address under the given randomizer. diff --git a/sdk/src/account/view_key.rs b/sdk/src/account/view_key.rs index 5c946ac..002d19c 100644 --- a/sdk/src/account/view_key.rs +++ b/sdk/src/account/view_key.rs @@ -49,6 +49,7 @@ impl ViewKey { } /// Returns the address corresponding to the view key. + #[getter] fn to_address(&self) -> Address { self.0.to_address().into() } From bdcecdbd2f3536634fd87eb151b100540f9c0984 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:23:38 -0500 Subject: [PATCH 10/72] feat: port programs value/key/program/transaction types to snarkvm 4.7.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Transition: rename is_bond() → is_bond_public(), is_unbond() → is_unbond_public() - Add #[getter] to ProgramID::name, network; Locator::program_id, name, network, resource; Program::id, functions, imports, source; Transition::id, program_id, function_name; Execution::execution_id; Response::outputs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/src/programs/execution.rs | 1 + sdk/src/programs/locator.rs | 4 ++++ sdk/src/programs/program.rs | 4 ++++ sdk/src/programs/program_id.rs | 2 ++ sdk/src/programs/response.rs | 1 + sdk/src/programs/transition.rs | 15 +++++++++------ 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/sdk/src/programs/execution.rs b/sdk/src/programs/execution.rs index 9c866fc..4877fe9 100644 --- a/sdk/src/programs/execution.rs +++ b/sdk/src/programs/execution.rs @@ -29,6 +29,7 @@ pub struct Execution(ExecutionNative); #[pymethods] impl Execution { /// Returns the Execution ID. + #[getter] fn execution_id(&self) -> anyhow::Result<Field> { self.0.to_execution_id().map(Into::into) } diff --git a/sdk/src/programs/locator.rs b/sdk/src/programs/locator.rs index 54d86c5..5d89abb 100644 --- a/sdk/src/programs/locator.rs +++ b/sdk/src/programs/locator.rs @@ -48,21 +48,25 @@ impl Locator { } /// Returns the program ID. + #[getter] fn program_id(&self) -> ProgramID { (*self.0.program_id()).into() } /// Returns the program name. + #[getter] fn name(&self) -> Identifier { (*self.0.name()).into() } /// Returns the network-level domain (NLD). + #[getter] fn network(&self) -> Identifier { (*self.0.network()).into() } /// Returns the resource name. + #[getter] fn resource(&self) -> Identifier { (*self.0.resource()).into() } diff --git a/sdk/src/programs/program.rs b/sdk/src/programs/program.rs index d323540..a69614d 100644 --- a/sdk/src/programs/program.rs +++ b/sdk/src/programs/program.rs @@ -42,11 +42,13 @@ impl Program { } /// Returns the id of the program + #[getter] fn id(&self) -> ProgramID { (*self.0.id()).into() } /// Returns all function names present in the program + #[getter] fn functions(&self) -> Vec<Identifier> { self.0 .functions() @@ -56,6 +58,7 @@ impl Program { } /// Returns the imports of the program + #[getter] fn imports(&self) -> Vec<ProgramID> { self.0 .imports() @@ -65,6 +68,7 @@ impl Program { } /// Returns the source code of the program + #[getter] fn source(&self) -> String { self.0.to_string() } diff --git a/sdk/src/programs/program_id.rs b/sdk/src/programs/program_id.rs index 94ed41e..abfe676 100644 --- a/sdk/src/programs/program_id.rs +++ b/sdk/src/programs/program_id.rs @@ -39,11 +39,13 @@ impl ProgramID { } /// Returns the program name. + #[getter] fn name(&self) -> Identifier { (*self.0.name()).into() } /// Returns the network-level domain (NLD). + #[getter] fn network(&self) -> Identifier { (*self.0.network()).into() } diff --git a/sdk/src/programs/response.rs b/sdk/src/programs/response.rs index 4e489e9..6d89da9 100644 --- a/sdk/src/programs/response.rs +++ b/sdk/src/programs/response.rs @@ -25,6 +25,7 @@ pub struct Response(ResponseNative); #[pymethods] impl Response { /// Returns the function outputs. + #[getter] fn outputs(&self) -> Vec<Value> { self.0.outputs().iter().cloned().map(Into::into).collect() } diff --git a/sdk/src/programs/transition.rs b/sdk/src/programs/transition.rs index 5981dd8..ba7890a 100644 --- a/sdk/src/programs/transition.rs +++ b/sdk/src/programs/transition.rs @@ -29,28 +29,31 @@ pub struct Transition(TransitionNative); #[pymethods] impl Transition { /// Returns the transition ID. + #[getter] fn id(&self) -> String { self.0.id().to_string() } /// Returns the program ID. + #[getter] fn program_id(&self) -> ProgramID { (*self.0.program_id()).into() } /// Returns the function name. + #[getter] fn function_name(&self) -> Identifier { (*self.0.function_name()).into() } - /// Returns true if this is a bond transition. - fn is_bond(&self) -> bool { - self.0.is_bond() + /// Returns true if this is a bond_public transition. + fn is_bond_public(&self) -> bool { + self.0.is_bond_public() } - /// Returns true if this is an unbond transition. - fn is_unbond(&self) -> bool { - self.0.is_unbond() + /// Returns true if this is an unbond_public transition. + fn is_unbond_public(&self) -> bool { + self.0.is_unbond_public() } /// Returns true if this is a fee_private transition. From 0bf686e6b26d86c169c652b49e2c4db632e92cf0 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:34:20 -0500 Subject: [PATCH 11/72] feat: port process/trace/query/fee to snarkvm 4.7.3 fee+cost+proving flow - query.rs: QueryNative::try_from(url) replaces removed From<String> - trace.rs: prove_execution/prove_fee take VarunaVersion::V2; prepare takes &QueryNative; rng via rand::make_rng - process.rs: interior-mutability receivers (&self) for add_program/insert_*_key; add_program routes through Process::lock(); fee authorize uses (base_fee, priority_fee, id) plain-u64 signature; execution_cost uses free fn with ConsensusVersion::V15; verify_execution/verify_fee take (ConsensusVersion, VarunaVersion, InclusionVersion); ProcessAuthError/ProcessExecError mapped to anyhow - fee.rs: payer/transition become #[getter] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- sdk/src/programs/fee.rs | 2 + sdk/src/programs/process.rs | 119 ++++++++++++++++++------------------ sdk/src/programs/query.rs | 4 +- sdk/src/programs/trace.rs | 13 ++-- 4 files changed, 71 insertions(+), 67 deletions(-) diff --git a/sdk/src/programs/fee.rs b/sdk/src/programs/fee.rs index 7fe4ba5..4651ab1 100644 --- a/sdk/src/programs/fee.rs +++ b/sdk/src/programs/fee.rs @@ -39,11 +39,13 @@ impl Fee { } /// Returns the payer, if the fee is public. + #[getter] fn payer(&self) -> Option<Address> { self.0.payer().map(Into::into) } /// Returns the transition. + #[getter] fn transition(&self) -> Transition { self.0.transition().clone().into() } diff --git a/sdk/src/programs/process.rs b/sdk/src/programs/process.rs index 55bc0b3..5d27760 100644 --- a/sdk/src/programs/process.rs +++ b/sdk/src/programs/process.rs @@ -16,13 +16,16 @@ use crate::{ types::{CurrentAleo, ProcessNative}, - Authorization, Execution, Fee, Field, Identifier, MicroCredits, PrivateKey, Program, ProgramID, - ProvingKey, RecordPlaintext, Response, Trace, Value, VerifyingKey, + Authorization, Execution, Fee, Field, Identifier, PrivateKey, Program, ProgramID, ProvingKey, + RecordPlaintext, Response, Trace, Value, VerifyingKey, }; +use indexmap::IndexMap; use pyo3::prelude::*; -use rand::{rngs::StdRng, SeedableRng}; -use snarkvm::prelude::cost_in_microcredits; +use rand::rngs::StdRng; +use snarkvm::algorithms::snark::varuna::VarunaVersion; +use snarkvm::console::network::ConsensusVersion; +use snarkvm::synthesizer::process::{execution_cost, InclusionVersion}; /// The Aleo process type. #[pyclass] @@ -37,8 +40,8 @@ impl Process { } /// Adds a new program to the process - fn add_program(&mut self, program: &Program) -> anyhow::Result<()> { - self.0.add_program(program) + fn add_program(&self, program: &Program) -> anyhow::Result<()> { + self.0.lock().add_program(program) } /// Returns true if the process contains the program with the given ID. @@ -59,7 +62,7 @@ impl Process { /// Inserts the given proving key, for the given program ID and function name. fn insert_proving_key( - &mut self, + &self, program_id: &ProgramID, function_name: &Identifier, proving_key: ProvingKey, @@ -81,7 +84,7 @@ impl Process { /// Inserts the given verifying key, for the given program ID and function name. fn insert_verifying_key( - &mut self, + &self, program_id: &ProgramID, function_name: &Identifier, verifying_key: VerifyingKey, @@ -104,105 +107,99 @@ impl Process { program_id, function_name, inputs.into_iter(), - &mut StdRng::from_entropy(), + &mut rand::make_rng::<StdRng>(), ) .map(Into::into) + .map_err(anyhow::Error::from) } - /// Authorizes the fee given the credits record, the fee amount (in microcredits), and the deployment or execution ID. + /// Authorizes the fee given the credits record, the base and priority fee amounts (in + /// microcredits), and the deployment or execution ID. fn authorize_fee_private( &self, private_key: &PrivateKey, credits: RecordPlaintext, - base_fee: MicroCredits, + base_fee: u64, + priority_fee: u64, deployment_or_execution_id: Field, - priority_fee: Option<MicroCredits>, ) -> anyhow::Result<Authorization> { self.0 .authorize_fee_private::<CurrentAleo, _>( private_key, credits.into(), - base_fee.into(), - priority_fee.map(Into::into).unwrap_or(0), + base_fee, + priority_fee, deployment_or_execution_id.into(), - &mut StdRng::from_entropy(), + &mut rand::make_rng::<StdRng>(), ) .map(Into::into) + .map_err(anyhow::Error::from) } - /// Authorizes the fee given the the fee amount (in microcredits) and the deployment or execution ID. + /// Authorizes the fee given the base and priority fee amounts (in microcredits) and the + /// deployment or execution ID. fn authorize_fee_public( &self, private_key: &PrivateKey, - base_fee: MicroCredits, + base_fee: u64, + priority_fee: u64, deployment_or_execution_id: Field, - priority_fee: Option<MicroCredits>, ) -> anyhow::Result<Authorization> { self.0 .authorize_fee_public::<CurrentAleo, _>( private_key, - base_fee.into(), - priority_fee.map(Into::into).unwrap_or(0), + base_fee, + priority_fee, deployment_or_execution_id.into(), - &mut StdRng::from_entropy(), + &mut rand::make_rng::<StdRng>(), ) .map(Into::into) + .map_err(anyhow::Error::from) } /// Executes the given authorization. fn execute(&self, authorization: Authorization) -> anyhow::Result<(Response, Trace)> { self.0 - .execute::<CurrentAleo, _>(authorization.into(), &mut StdRng::from_entropy()) + .execute::<CurrentAleo, _>(authorization.into(), &mut rand::make_rng::<StdRng>()) .map(|(r, t)| (Response::from(r), Trace::from(t))) + .map_err(anyhow::Error::from) } /// Verifies the given execution is valid. Note: This does not check that the global state root exists in the ledger. fn verify_execution(&self, execution: &Execution) -> anyhow::Result<()> { - self.0.verify_execution(execution) + let execution: crate::types::ExecutionNative = execution.clone().into(); + // Build the map of program stacks referenced by the execution's transitions. + let execution_stacks = execution + .transitions() + .map(|transition| { + Ok(( + *transition.program_id(), + self.0.get_stack(transition.program_id())?, + )) + }) + .collect::<anyhow::Result<IndexMap<_, _>>>()?; + ProcessNative::verify_execution( + ConsensusVersion::V15, + VarunaVersion::V2, + InclusionVersion::V1, + &execution, + &execution_stacks, + ) } /// Verifies the given fee is valid. Note: This does not check that the global state root exists in the ledger. fn verify_fee(&self, fee: &Fee, deployment_or_execution_id: Field) -> anyhow::Result<()> { - self.0.verify_fee(fee, deployment_or_execution_id.into()) + self.0.verify_fee( + ConsensusVersion::V15, + VarunaVersion::V2, + InclusionVersion::V1, + fee, + deployment_or_execution_id.into(), + ) } /// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). - fn execution_cost( - &self, - execution: &Execution, - ) -> anyhow::Result<(MicroCredits, (MicroCredits, MicroCredits))> { - // Compute the storage cost in microcredits. - let storage_cost = execution.size_in_bytes()?; - - // Compute the finalize cost in microcredits. - let mut finalize_cost = 0u64; - - for transition in execution.transitions() { - let program = self.0.get_program(transition.program_id())?; - let function = program.get_function(transition.function_name())?; - let cost = match function.finalize_logic() { - Some(finalize) => cost_in_microcredits(finalize)?, - None => continue, - }; - // Accumulate the finalize cost. - finalize_cost = finalize_cost.checked_add(cost).ok_or(anyhow::anyhow!( - "The finalize cost computation overflowed for an execution" - ))?; - } - - // Compute the total cost in microcredits. - let total_cost = storage_cost - .checked_add(finalize_cost) - .ok_or(anyhow::anyhow!( - "The total cost computation overflowed for an execution" - ))?; - - Ok(( - MicroCredits::from(total_cost), - ( - MicroCredits::from(storage_cost), - MicroCredits::from(finalize_cost), - ), - )) + fn execution_cost(&self, execution: &Execution) -> anyhow::Result<(u64, (u64, u64))> { + execution_cost(&self.0, &execution.clone().into(), ConsensusVersion::V15) } } diff --git a/sdk/src/programs/query.rs b/sdk/src/programs/query.rs index b9f12fb..741e9c4 100644 --- a/sdk/src/programs/query.rs +++ b/sdk/src/programs/query.rs @@ -27,8 +27,8 @@ pub struct Query(QueryNative); impl Query { /// The base URL of the node. #[staticmethod] - fn rest(url: String) -> Self { - QueryNative::REST(url).into() + fn rest(url: String) -> anyhow::Result<Self> { + Ok(QueryNative::try_from(url)?.into()) } #[classattr] diff --git a/sdk/src/programs/trace.rs b/sdk/src/programs/trace.rs index 0c2860e..370e64f 100644 --- a/sdk/src/programs/trace.rs +++ b/sdk/src/programs/trace.rs @@ -20,7 +20,8 @@ use crate::{ }; use pyo3::prelude::*; -use rand::{rngs::StdRng, SeedableRng}; +use rand::rngs::StdRng; +use snarkvm::algorithms::snark::varuna::VarunaVersion; /// The Aleo trace type. #[pyclass] @@ -58,19 +59,23 @@ impl Trace { let locator: LocatorNative = locator.into(); let locator_s = locator.to_string(); self.0 - .prove_execution::<CurrentAleo, _>(&locator_s, &mut StdRng::from_entropy()) + .prove_execution::<CurrentAleo, _>( + &locator_s, + VarunaVersion::V2, + &mut rand::make_rng::<StdRng>(), + ) .map(Into::into) } /// Returns a new fee with a proof, for the current inclusion assignment and global state root. fn prove_fee(&self) -> anyhow::Result<Fee> { self.0 - .prove_fee::<CurrentAleo, _>(&mut StdRng::from_entropy()) + .prove_fee::<CurrentAleo, _>(VarunaVersion::V2, &mut rand::make_rng::<StdRng>()) .map(Into::into) } fn prepare(&mut self, query: Query) -> anyhow::Result<()> { - self.0.prepare(QueryNative::from(query)) + self.0.prepare(&QueryNative::from(query)) } } From 35ce0e8c7fbe2f8d16bd29633396988fd5d413bc Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:40:39 -0500 Subject: [PATCH 12/72] feat: expose aleo.mainnet re-export over _aleolib_mainnet - Create sdk/python/aleo/mainnet.py with wildcard re-export from _aleolib_mainnet - Update sdk/pyproject.toml module-name from aleo._aleolib to aleo._aleolib_mainnet - Update sdk/python/aleo/__init__.py to import mainnet submodule (no star import) - Update sdk/python/aleo/encryptor.py to import from .mainnet instead of top-level . - Update sdk/.gitignore to cover all *.abi3.so build artifacts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/.gitignore | 2 +- sdk/pyproject.toml | 2 +- sdk/python/aleo/__init__.py | 4 +--- sdk/python/aleo/encryptor.py | 2 +- sdk/python/aleo/mainnet.py | 2 ++ 5 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 sdk/python/aleo/mainnet.py diff --git a/sdk/.gitignore b/sdk/.gitignore index c9f96e6..c5699e1 100644 --- a/sdk/.gitignore +++ b/sdk/.gitignore @@ -1,5 +1,5 @@ .env/ target/ __pycache__/ -python/aleo/_aleolib.abi3.so +python/aleo/*.abi3.so _readthedocs/ diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index daeddda..d445f40 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -22,7 +22,7 @@ build-backend = "maturin" [tool.maturin] python-source = "python" -module-name = "aleo._aleolib" +module-name = "aleo._aleolib_mainnet" [tool.pyright] reportMissingModuleSource = false # don't report missing aleo.so for CI purposes diff --git a/sdk/python/aleo/__init__.py b/sdk/python/aleo/__init__.py index bdcd128..90d7d5e 100644 --- a/sdk/python/aleo/__init__.py +++ b/sdk/python/aleo/__init__.py @@ -1,6 +1,4 @@ from __future__ import annotations -from ._aleolib import * +from . import mainnet from .encryptor import * - -__doc__ = _aleolib.__doc__ diff --git a/sdk/python/aleo/encryptor.py b/sdk/python/aleo/encryptor.py index 69bc292..01b3fe2 100644 --- a/sdk/python/aleo/encryptor.py +++ b/sdk/python/aleo/encryptor.py @@ -1,6 +1,6 @@ from __future__ import annotations -from . import PrivateKey, Ciphertext, Field, Network, Identifier, Plaintext, Literal +from .mainnet import PrivateKey, Ciphertext, Field, Network, Identifier, Plaintext, Literal class Encryptor: diff --git a/sdk/python/aleo/mainnet.py b/sdk/python/aleo/mainnet.py new file mode 100644 index 0000000..592446f --- /dev/null +++ b/sdk/python/aleo/mainnet.py @@ -0,0 +1,2 @@ +"""MainnetV0 bindings (compiled extension module _aleolib_mainnet).""" +from ._aleolib_mainnet import * # noqa: F401,F403 From 22ee01f086f8b66ec521685a22fe998f6cfc9552 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:42:54 -0500 Subject: [PATCH 13/72] test: vendor TS-SDK known-answer vectors + pytest scaffolding Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/pytest.ini | 3 +++ sdk/python/tests/conftest.py | 7 +++++++ sdk/python/tests/vectors/accounts.json | 15 +++++++++++++++ sdk/python/tests/vectors/records.json | 11 +++++++++++ 4 files changed, 36 insertions(+) create mode 100644 sdk/pytest.ini create mode 100644 sdk/python/tests/conftest.py create mode 100644 sdk/python/tests/vectors/accounts.json create mode 100644 sdk/python/tests/vectors/records.json diff --git a/sdk/pytest.ini b/sdk/pytest.ini new file mode 100644 index 0000000..ae50458 --- /dev/null +++ b/sdk/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = python/tests +python_files = test_*.py diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py new file mode 100644 index 0000000..a8a7748 --- /dev/null +++ b/sdk/python/tests/conftest.py @@ -0,0 +1,7 @@ +import json +from pathlib import Path + +VECTORS = Path(__file__).parent / "vectors" + +def load_vectors(name: str) -> dict: + return json.loads((VECTORS / name).read_text()) diff --git a/sdk/python/tests/vectors/accounts.json b/sdk/python/tests/vectors/accounts.json new file mode 100644 index 0000000..a77b2f4 --- /dev/null +++ b/sdk/python/tests/vectors/accounts.json @@ -0,0 +1,15 @@ +{ + "_source": "github.com/ProvableHQ/sdk@543b41e0a7e3d6a8a49ae8d0870809dc92b88684", + "triples": [ + {"private_key": "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH", + "view_key": "AViewKey1mSnpFFC8Mj4fXbK5YiWgZ3mjiV8CxA79bYNa8ymUpTrw", + "address": "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px"}, + {"private_key": "APrivateKey1zkp3dQx4WASWYQVWKkq14v3RoQDfY2kbLssUj7iifi1VUQ6", + "view_key": "AViewKey1cxguxtKkjYnT9XDza9yTvVMxt6Ckb1Pv4ck1hppMzmCB", + "address": "aleo184vuwr5u7u0ha5f5k44067dd2uaqewxx6pe5ltha5pv99wvhfqxqv339h4"}, + {"private_key": "APrivateKey1zkpJkyYRGYtkeHDaFfwsKtUJzia7csiWhfBWPXWhXJzy9Ls", + "view_key": "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX", + "address": "aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3"} + ], + "message_bytes": [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] +} diff --git a/sdk/python/tests/vectors/records.json b/sdk/python/tests/vectors/records.json new file mode 100644 index 0000000..eb4f3bc --- /dev/null +++ b/sdk/python/tests/vectors/records.json @@ -0,0 +1,11 @@ +{ + "_source": "github.com/ProvableHQ/sdk@543b41e0a7e3d6a8a49ae8d0870809dc92b88684", + "decrypt_kat": { + "view_key": "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX", + "non_owner_view_key": "AViewKey1e2WyreaH5H4RBcioLL2GnxvHk5Ud46EtwycnhTdXLmXp", + "ciphertext": "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q", + "owner": "aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3", + "nonce": "3077450429259593211617823051143573281856129402760267155982965992208217472983group", + "microcredits": 1500000000000000 + } +} From b8104cfea59130dc5711587bdb36c71f5bfedad1 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:45:26 -0500 Subject: [PATCH 14/72] test: account key-derivation and sign/verify KATs --- sdk/python/tests/test_account.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 sdk/python/tests/test_account.py diff --git a/sdk/python/tests/test_account.py b/sdk/python/tests/test_account.py new file mode 100644 index 0000000..65a42ed --- /dev/null +++ b/sdk/python/tests/test_account.py @@ -0,0 +1,18 @@ +from aleo.mainnet import PrivateKey, Signature +from conftest import load_vectors + + +def test_key_derivation_triples(): + for t in load_vectors("accounts.json")["triples"]: + pk = PrivateKey.from_string(t["private_key"]) + assert str(pk.view_key) == t["view_key"] + assert str(pk.address) == t["address"] + + +def test_sign_verify_roundtrip(): + msg = bytes(load_vectors("accounts.json")["message_bytes"]) + pk = PrivateKey.random() + sig = pk.sign(msg) + # verify lives on Signature: sig.verify(address, message) -> bool + assert sig.verify(pk.address, msg) is True + assert sig.verify(pk.address, b"tampered") is False From 2f5701e4ccfdc0197feb7392ef3263f1d7ad7111 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:48:06 -0500 Subject: [PATCH 15/72] test: record decrypt KAT (version-aware Record) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Historical ciphertext from records.json (sdk@543b41e) parsed and decrypted successfully under snarkvm 4.7.3 — KAT path taken, no fallback needed. Assertions: - is_owner(vk) True / is_owner(non_owner_vk) False - pt.owner (stripped of .private qualifier) == vector owner address - pt.nonce == vector nonce - pt.version == 0 (v0-era record; snarkvm 4.7.3 exposes _version=0u8) - f"{microcredits}u64" in str(pt) — checks decrypted plaintext content against the vector value, not just vector-internal consistency Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_records.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 sdk/python/tests/test_records.py diff --git a/sdk/python/tests/test_records.py b/sdk/python/tests/test_records.py new file mode 100644 index 0000000..f863148 --- /dev/null +++ b/sdk/python/tests/test_records.py @@ -0,0 +1,28 @@ +import pytest +from aleo.mainnet import ViewKey, RecordCiphertext +from conftest import load_vectors + + +def test_record_decrypt_kat(): + v = load_vectors("records.json")["decrypt_kat"] + ct = RecordCiphertext.from_string(v["ciphertext"]) + vk = ViewKey.from_string(v["view_key"]) + + # Ownership checks + assert ct.is_owner(vk) is True + assert ct.is_owner(ViewKey.from_string(v["non_owner_view_key"])) is False + + # Decrypt and verify plaintext fields + pt = ct.decrypt(vk) + + # str(pt.owner) returns "address.private" — strip the visibility qualifier + assert str(pt.owner).split(".")[0] == v["owner"] + assert str(pt.nonce) == v["nonce"] + + # Version: this is a v0-era record; snarkvm 4.7.3 reports _version=0u8 + assert isinstance(pt.version, int) + assert pt.version == 0 + + # Microcredits: assert the decrypted plaintext contains the expected value, + # not just that the vector has the right constant (str(pt) contains "Xu64") + assert f"{v['microcredits']}u64" in str(pt) From 00d3a36da7b40121339218e6edbbc32186e95a07 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:49:41 -0500 Subject: [PATCH 16/72] test: algebra round-trip and arithmetic KATs --- sdk/python/tests/test_algebra.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 sdk/python/tests/test_algebra.py diff --git a/sdk/python/tests/test_algebra.py b/sdk/python/tests/test_algebra.py new file mode 100644 index 0000000..e94b4a2 --- /dev/null +++ b/sdk/python/tests/test_algebra.py @@ -0,0 +1,15 @@ +from aleo.mainnet import Field, Group, Scalar, Boolean + +def test_field_roundtrip(): + assert str(Field.from_string("1field")) == "1field" + +def test_group_roundtrip(): + assert str(Group.from_string("2group")) == "2group" + +def test_field_arithmetic(): + a = Field.from_string("2field") + b = Field.from_string("3field") + assert str(a * b) == "6field" + +def test_boolean(): + assert bool(Boolean(True)) is True From 7da12e400665f2f77003e4fecbe8dc55f3a392bd Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:52:35 -0500 Subject: [PATCH 17/72] test: programs parse + authorize smoke tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_programs.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 sdk/python/tests/test_programs.py diff --git a/sdk/python/tests/test_programs.py b/sdk/python/tests/test_programs.py new file mode 100644 index 0000000..6381db3 --- /dev/null +++ b/sdk/python/tests/test_programs.py @@ -0,0 +1,22 @@ +from aleo.mainnet import Program, Process, PrivateKey, Identifier, ProgramID, Value, Authorization + + +def test_credits_program_parse(): + p = Program.credits() + assert str(p.id) == "credits.aleo" + assert Identifier.from_string("transfer_public") in p.functions + + +def test_authorize_transfer_public(): + process = Process.load() + pk = PrivateKey.random() + recipient = str(pk.address) + auth = process.authorize( + pk, + ProgramID.from_string("credits.aleo"), + Identifier.from_string("transfer_public"), + [Value.parse(f"{recipient}"), Value.parse("10u64")], + ) + assert auth is not None + # round-trip the authorization + assert str(Authorization.from_json(auth.to_json())) == str(auth) From ccac109c8b75b42c8b2ac6b394ce6b2a2edffd25 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 11:57:50 -0500 Subject: [PATCH 18/72] ci: run pytest suite; drop legacy test.py; retarget type stubs - Delete sdk/python/test.py (legacy ad-hoc unittest referencing removed APIs) - git mv _aleolib.pyi -> _aleolib_mainnet.pyi; add mainnet.pyi re-export stub - Drop coinbase symbols (CoinbasePuzzle, CoinbaseVerifyingKey, EpochChallenge, ProverSolution) from stubs - Apply API deltas: random() staticmethods, property accessors, renamed is_bond_public/is_unbond_public, updated authorize_fee signatures, Trace.prove_execution(locator: str), Network.edition removed - CI: maturin develop --features mainnet; replace python3 test.py with pip install pytest + python -m pytest python/tests -v; fix double-cd (working_directory already in sdk/) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .circleci/config.yml | 7 +- sdk/python/aleo/__init__.pyi | 116 ++++--- sdk/python/aleo/_aleolib.pyi | 193 ----------- sdk/python/aleo/_aleolib_mainnet.pyi | 457 +++++++++++++++++++++++++++ sdk/python/aleo/mainnet.pyi | 1 + sdk/python/test.py | 135 -------- 6 files changed, 515 insertions(+), 394 deletions(-) delete mode 100644 sdk/python/aleo/_aleolib.pyi create mode 100644 sdk/python/aleo/_aleolib_mainnet.pyi create mode 100644 sdk/python/aleo/mainnet.pyi delete mode 100644 sdk/python/test.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 0b7cd0e..8d268e7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,15 +28,14 @@ jobs: name: Build and develop the package command: | . .env/bin/activate - cd sdk - maturin develop + maturin develop --features mainnet # Run the unittests - run: name: Run tests command: | . .env/bin/activate - cd sdk - python3 test.py + pip install pytest + python -m pytest python/tests -v workflows: version: 2 diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index dfc7823..c432e33 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -3,10 +3,15 @@ from typing import List, Mapping, Optional, Tuple class Account: + @staticmethod + def random() -> Account: ... @staticmethod def from_private_key(private_key: PrivateKey) -> Account: ... + @property def private_key(self) -> PrivateKey: ... + @property def view_key(self) -> ViewKey: ... + @property def address(self) -> Address: ... def sign(self, message: bytes) -> Signature: ... def verify(self, signature: Signature, message: bytes) -> bool: ... @@ -44,38 +49,23 @@ class Credits: def micro(self) -> MicroCredits: ... -class CoinbasePuzzle: - @staticmethod - def load() -> CoinbasePuzzle: ... - def verifying_key(self) -> CoinbaseVerifyingKey: ... - - -class CoinbaseVerifyingKey: - pass - - class ComputeKey: + @property def address(self) -> Address: ... def pk_sig(self) -> Group: ... def pr_sig(self) -> Group: ... def sk_prf(self) -> Scalar: ... + class Encryptor: @staticmethod def encrypt_private_key_with_secret(private_key: PrivateKey, secret: str) -> Ciphertext: ... @staticmethod def decrypt_private_key_with_secret(ciphertext: Ciphertext, secret: str) -> PrivateKey: ... -class EpochChallenge: - @staticmethod - def from_json(json: str) -> EpochChallenge: ... - def to_json(self) -> str: ... - @staticmethod - def from_bytes(bytes: bytes) -> EpochChallenge: ... - def bytes(self) -> bytes: ... - class Execution: + @property def execution_id(self) -> Field: ... @staticmethod def from_json(json: str) -> Execution: ... @@ -88,7 +78,9 @@ class Execution: class Fee: def is_fee_private(self) -> bool: ... def is_fee_public(self) -> bool: ... + @property def payer(self) -> Optional[Address]: ... + @property def transition(self) -> Transition: ... @staticmethod def from_json(json: str) -> Fee: ... @@ -202,9 +194,13 @@ class Locator: def __new__(cls, program_id: ProgramID, resource: Identifier) -> Locator: ... @staticmethod def from_string(s: str) -> Locator: ... + @property def program_id(self) -> ProgramID: ... + @property def name(self) -> Identifier: ... + @property def network(self) -> Identifier: ... + @property def resource(self) -> Identifier: ... @@ -214,8 +210,6 @@ class Network: @staticmethod def id() -> int: ... @staticmethod - def edition() -> int: ... - @staticmethod def hash_psd2(input: List[Field]) -> Field: ... @@ -244,17 +238,23 @@ class Plaintext: class PrivateKey: - def address(self) -> Address: ... - def compute_key(self) -> ComputeKey: ... + @staticmethod + def random() -> PrivateKey: ... @staticmethod def from_string(private_key: str) -> PrivateKey: ... @staticmethod def from_seed(seed: Field) -> PrivateKey: ... + @property + def address(self) -> Address: ... + @property + def view_key(self) -> ViewKey: ... + @property + def compute_key(self) -> ComputeKey: ... + @property def seed(self) -> Field: ... def sign(self, message: bytes) -> Signature: ... def sk_sig(self) -> Scalar: ... def r_sig(self) -> Scalar: ... - def view_key(self) -> ViewKey: ... class Process: @@ -262,32 +262,20 @@ class Process: def load() -> Process: ... def add_program(self, program: Program) -> None: ... def contains_program(self, program_id: ProgramID) -> bool: ... - - def get_proving_key(self, program_id: ProgramID, - function_name: Identifier) -> ProvingKey: ... - - def insert_proving_key(self, program_id: ProgramID, - function_name: Identifier, proving_key: ProvingKey) -> None: ... - - def get_verifying_key(self, program_id: ProgramID, - function_name: Identifier) -> VerifyingKey: ... - - def insert_verifying_key(self, program_id: ProgramID, - function_name: Identifier, verifying_key: VerifyingKey) -> None: ... - + def get_proving_key(self, program_id: ProgramID, function_name: Identifier) -> ProvingKey: ... + def insert_proving_key(self, program_id: ProgramID, function_name: Identifier, proving_key: ProvingKey) -> None: ... + def get_verifying_key(self, program_id: ProgramID, function_name: Identifier) -> VerifyingKey: ... + def insert_verifying_key(self, program_id: ProgramID, function_name: Identifier, verifying_key: VerifyingKey) -> None: ... def authorize(self, private_key: PrivateKey, program_id: ProgramID, function_name: Identifier, inputs: List[Value]) -> Authorization: ... - - def authorize_fee_private(self, private_key: PrivateKey, credits: RecordPlaintext, base_fee: MicroCredits, - deployment_or_execution_id: Field, priority_fee: Optional[MicroCredits]) -> Authorization: ... - def authorize_fee_public(self, private_key: PrivateKey, base_fee: MicroCredits, - deployment_or_execution_id: Field, priority_fee: Optional[MicroCredits]) -> Authorization: ... - + def authorize_fee_private(self, private_key: PrivateKey, credits: RecordPlaintext, base_fee: int, + priority_fee: int, deployment_or_execution_id: Field) -> Authorization: ... + def authorize_fee_public(self, private_key: PrivateKey, base_fee: int, + priority_fee: int, deployment_or_execution_id: Field) -> Authorization: ... def execute(self, authorization: Authorization) -> Tuple[Response, Trace]: ... def verify_execution(self, execution: Execution) -> None: ... def verify_fee(self, fee: Fee, deployment_or_execution_id: Field) -> None: ... - def execution_cost( - self, execution: Execution) -> Tuple[MicroCredits, Tuple[MicroCredits, MicroCredits]]: ... + def execution_cost(self, execution: Execution) -> Tuple[MicroCredits, Tuple[MicroCredits, MicroCredits]]: ... class Program: @@ -295,33 +283,26 @@ class Program: def from_source(s: str) -> Program: ... @staticmethod def credits() -> Program: ... + @property def id(self) -> ProgramID: ... + @property def functions(self) -> List[Identifier]: ... + @property def imports(self) -> List[ProgramID]: ... + @property def source(self) -> str: ... class ProgramID: @staticmethod def from_string(s: str) -> ProgramID: ... + @property def name(self) -> Identifier: ... + @property def network(self) -> Identifier: ... def is_aleo(self) -> bool: ... -class ProverSolution: - def address(self) -> Address: ... - def verify(self, verifying_key: CoinbaseVerifyingKey, - epoch_challenge: EpochChallenge, proof_target: int) -> bool: ... - - @staticmethod - def from_json(json: str) -> ProverSolution: ... - def to_json(self) -> str: ... - @staticmethod - def from_bytes(bytes: bytes) -> ProverSolution: ... - def bytes(self) -> bytes: ... - - class ProvingKey: @staticmethod def from_string(s: str) -> ProvingKey: ... @@ -345,13 +326,18 @@ class RecordCiphertext: class RecordPlaintext: @staticmethod def from_string(s: str) -> RecordPlaintext: ... + @property def owner(self) -> str: ... + @property def nonce(self) -> Group: ... + @property + def version(self) -> int: ... def serial_number(self, private_key: PrivateKey, program_id: ProgramID, - record_identifier: Identifier) -> Field: ... + record_identifier: Identifier, record_view_key: Field) -> Field: ... class Response: + @property def outputs(self) -> List[Value]: ... @@ -363,11 +349,14 @@ class Scalar: class Signature: + @property def challenge(self) -> Scalar: ... + @property def compute_key(self) -> ComputeKey: ... + @property + def response(self) -> Scalar: ... @staticmethod def from_string(s: str) -> Signature: ... - def response(self) -> Scalar: ... @staticmethod def sign(private_key: PrivateKey, message: bytes) -> Signature: ... def verify(self, address: Address, message: bytes) -> bool: ... @@ -378,7 +367,7 @@ class Trace: def is_fee_private(self) -> bool: ... def is_fee_public(self) -> bool: ... def transitions(self) -> List[Transition]: ... - def prove_execution(self, locator: Locator) -> Execution: ... + def prove_execution(self, locator: str) -> Execution: ... def prove_fee(self) -> Fee: ... def prepare(self, query: Query) -> None: ... @@ -395,11 +384,14 @@ class Transaction: class Transition: + @property def id(self) -> str: ... + @property def program_id(self) -> ProgramID: ... + @property def function_name(self) -> Identifier: ... - def is_bond(self) -> bool: ... - def is_unbond(self) -> bool: ... + def is_bond_public(self) -> bool: ... + def is_unbond_public(self) -> bool: ... def is_fee_private(self) -> bool: ... def is_fee_public(self) -> bool: ... def is_split(self) -> bool: ... diff --git a/sdk/python/aleo/_aleolib.pyi b/sdk/python/aleo/_aleolib.pyi deleted file mode 100644 index 211c2eb..0000000 --- a/sdk/python/aleo/_aleolib.pyi +++ /dev/null @@ -1,193 +0,0 @@ -from __future__ import annotations - -class Account: - pass - - -class Address: - pass - - -class Authorization: - pass - - -class Boolean: - pass - - -class Ciphertext: - pass - - -class Credits: - pass - - -class CoinbasePuzzle: - pass - - -class CoinbaseVerifyingKey: - pass - - -class ComputeKey: - pass - - -class EpochChallenge: - pass - - -class Execution: - pass - - -class Fee: - pass - - -class Field: - pass - - -class Group: - pass - - -class Identifier: - pass - - -class I8: - pass - - -class I16: - pass - - -class I32: - pass - - -class I64: - pass - - -class I128: - pass - - -class Literal: - pass - - -class Locator: - pass - - -class Network: - pass - - -class MicroCredits: - pass - - -class Plaintext: - pass - - -class PrivateKey: - pass - - -class Process: - pass - - -class Program: - pass - - -class ProgramID: - pass - - -class ProverSolution: - pass - - -class ProvingKey: - pass - - -class Query: - pass - - -class RecordCiphertext: - pass - - -class RecordPlaintext: - pass - - -class Response: - pass - - -class Scalar: - pass - - -class Signature: - pass - - -class Trace: - pass - - -class Transaction: - pass - - -class Transition: - pass - - -class U8: - pass - - -class U16: - pass - - -class U32: - pass - - -class U64: - pass - - -class U128: - pass - - -class Value: - pass - - -class VerifyingKey: - pass - - -class ViewKey: - pass - diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi new file mode 100644 index 0000000..6594fb3 --- /dev/null +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -0,0 +1,457 @@ +from __future__ import annotations + +class Account: + @staticmethod + def random() -> Account: ... + @staticmethod + def from_private_key(private_key: PrivateKey) -> Account: ... + @property + def private_key(self) -> PrivateKey: ... + @property + def view_key(self) -> ViewKey: ... + @property + def address(self) -> Address: ... + def sign(self, message: bytes) -> Signature: ... + def verify(self, signature: Signature, message: bytes) -> bool: ... + def decrypt(self, record_ciphertext: RecordCiphertext) -> RecordPlaintext: ... + def is_owner(self, record_ciphertext: RecordCiphertext) -> bool: ... + + +class Address: + @staticmethod + def from_string(s: str) -> Address: ... + + +class Authorization: + @staticmethod + def from_json(json: str) -> Authorization: ... + def to_json(self) -> str: ... + @staticmethod + def from_bytes(bytes: bytes) -> Authorization: ... + def bytes(self) -> bytes: ... + + +class Boolean: + def __new__(cls, b: bool) -> Boolean: ... + + +class Ciphertext: + @staticmethod + def from_string(s: str) -> Ciphertext: ... + def decrypt(self, view_key: ViewKey, nonce: Group) -> Plaintext: ... + def decrypt_symmetric(self, plaintext_view_key: Field) -> Plaintext: ... + + +class Credits: + def __new__(cls, value: float) -> Credits: ... + def micro(self) -> MicroCredits: ... + + +class ComputeKey: + @property + def address(self) -> Address: ... + def pk_sig(self) -> Group: ... + def pr_sig(self) -> Group: ... + def sk_prf(self) -> Scalar: ... + + +class Encryptor: + @staticmethod + def encrypt_private_key_with_secret(private_key: PrivateKey, secret: str) -> Ciphertext: ... + @staticmethod + def decrypt_private_key_with_secret(ciphertext: Ciphertext, secret: str) -> PrivateKey: ... + + +class Execution: + @property + def execution_id(self) -> Field: ... + @staticmethod + def from_json(json: str) -> Execution: ... + def to_json(self) -> str: ... + @staticmethod + def from_bytes(bytes: bytes) -> Execution: ... + def bytes(self) -> bytes: ... + + +class Fee: + def is_fee_private(self) -> bool: ... + def is_fee_public(self) -> bool: ... + @property + def payer(self) -> Address | None: ... + @property + def transition(self) -> Transition: ... + @staticmethod + def from_json(json: str) -> Fee: ... + def to_json(self) -> str: ... + @staticmethod + def from_bytes(bytes: bytes) -> Fee: ... + def bytes(self) -> bytes: ... + + +class Field: + @staticmethod + def random() -> Field: ... + @staticmethod + def from_string(s: str) -> Field: ... + @staticmethod + def domain_separator(domain: str) -> Field: ... + @staticmethod + def from_u128(u128: int) -> Field: ... + @staticmethod + def zero() -> Field: ... + def __mul__(self, other: Field) -> Field: ... + def __truediv__(self, other: Field) -> Field: ... + + +class Group: + @staticmethod + def from_string(s: str) -> Group: ... + @staticmethod + def zero() -> Group: ... + + +class Identifier: + @staticmethod + def from_string(s: str) -> Identifier: ... + + +class I8: + def __new__(cls, value: int) -> I8: ... + @staticmethod + def zero() -> I8: ... + def __int__(self) -> int: ... + + +class I16: + def __new__(cls, value: int) -> I16: ... + @staticmethod + def zero() -> I16: ... + def __int__(self) -> int: ... + + +class I32: + def __new__(cls, value: int) -> I32: ... + @staticmethod + def zero() -> I32: ... + def __int__(self) -> int: ... + + +class I64: + def __new__(cls, value: int) -> I64: ... + @staticmethod + def zero() -> I64: ... + def __int__(self) -> int: ... + + +class I128: + def __new__(cls, value: int) -> I128: ... + @staticmethod + def zero() -> I128: ... + def __int__(self) -> int: ... + + +class Literal: + @staticmethod + def parse(s: str) -> Literal: ... + @staticmethod + def from_address(address: Address) -> Literal: ... + @staticmethod + def from_field(field: Field) -> Literal: ... + @staticmethod + def from_group(group: Group) -> Literal: ... + @staticmethod + def from_scalar(scalar: Scalar) -> Literal: ... + @staticmethod + def from_signature(signature: Signature) -> Literal: ... + @staticmethod + def from_boolean(boolean: Boolean) -> Literal: ... + @staticmethod + def from_i8(value: I8) -> Literal: ... + @staticmethod + def from_i16(value: I16) -> Literal: ... + @staticmethod + def from_i32(value: I32) -> Literal: ... + @staticmethod + def from_i64(value: I64) -> Literal: ... + @staticmethod + def from_i128(value: I128) -> Literal: ... + @staticmethod + def from_u8(value: U8) -> Literal: ... + @staticmethod + def from_u16(value: U16) -> Literal: ... + @staticmethod + def from_u32(value: U32) -> Literal: ... + @staticmethod + def from_u64(value: U64) -> Literal: ... + @staticmethod + def from_u128(value: U128) -> Literal: ... + def type_name(self) -> str: ... + + +class Locator: + def __new__(cls, program_id: ProgramID, resource: Identifier) -> Locator: ... + @staticmethod + def from_string(s: str) -> Locator: ... + @property + def program_id(self) -> ProgramID: ... + @property + def name(self) -> Identifier: ... + @property + def network(self) -> Identifier: ... + @property + def resource(self) -> Identifier: ... + + +class Network: + @staticmethod + def name() -> str: ... + @staticmethod + def id() -> int: ... + @staticmethod + def hash_psd2(input: list[Field]) -> Field: ... + + +class MicroCredits: + def __new__(cls, value: int) -> MicroCredits: ... + def __int__(self) -> int: ... + + +class Plaintext: + @staticmethod + def from_string(s: str) -> Plaintext: ... + @staticmethod + def new_literal(literal: Literal) -> Plaintext: ... + @staticmethod + def new_struct(kv: list[tuple[Identifier, Plaintext]]) -> Plaintext: ... + @staticmethod + def new_array(kv: list[Plaintext]) -> Plaintext: ... + def encrypt(self, address: Address, randomizer: Scalar) -> Ciphertext: ... + def encrypt_symmetric(self, plaintext_view_key: Field) -> Ciphertext: ... + def is_literal(self) -> bool: ... + def is_struct(self) -> bool: ... + def is_array(self) -> bool: ... + def as_literal(self) -> Literal: ... + def as_struct(self) -> dict[Identifier, Plaintext]: ... + def as_array(self) -> list[Plaintext]: ... + + +class PrivateKey: + @staticmethod + def random() -> PrivateKey: ... + @staticmethod + def from_string(private_key: str) -> PrivateKey: ... + @staticmethod + def from_seed(seed: Field) -> PrivateKey: ... + @property + def address(self) -> Address: ... + @property + def view_key(self) -> ViewKey: ... + @property + def compute_key(self) -> ComputeKey: ... + @property + def seed(self) -> Field: ... + def sign(self, message: bytes) -> Signature: ... + def sk_sig(self) -> Scalar: ... + def r_sig(self) -> Scalar: ... + + +class Process: + @staticmethod + def load() -> Process: ... + def add_program(self, program: Program) -> None: ... + def contains_program(self, program_id: ProgramID) -> bool: ... + def get_proving_key(self, program_id: ProgramID, function_name: Identifier) -> ProvingKey: ... + def insert_proving_key(self, program_id: ProgramID, function_name: Identifier, proving_key: ProvingKey) -> None: ... + def get_verifying_key(self, program_id: ProgramID, function_name: Identifier) -> VerifyingKey: ... + def insert_verifying_key(self, program_id: ProgramID, function_name: Identifier, verifying_key: VerifyingKey) -> None: ... + def authorize(self, private_key: PrivateKey, program_id: ProgramID, function_name: Identifier, inputs: list[Value]) -> Authorization: ... + def authorize_fee_private(self, private_key: PrivateKey, credits: RecordPlaintext, base_fee: int, priority_fee: int, deployment_or_execution_id: Field) -> Authorization: ... + def authorize_fee_public(self, private_key: PrivateKey, base_fee: int, priority_fee: int, deployment_or_execution_id: Field) -> Authorization: ... + def execute(self, authorization: Authorization) -> tuple[Response, Trace]: ... + def verify_execution(self, execution: Execution) -> None: ... + def verify_fee(self, fee: Fee, deployment_or_execution_id: Field) -> None: ... + def execution_cost(self, execution: Execution) -> tuple[MicroCredits, tuple[MicroCredits, MicroCredits]]: ... + + +class Program: + @staticmethod + def from_source(s: str) -> Program: ... + @staticmethod + def credits() -> Program: ... + @property + def id(self) -> ProgramID: ... + @property + def functions(self) -> list[Identifier]: ... + @property + def imports(self) -> list[ProgramID]: ... + @property + def source(self) -> str: ... + + +class ProgramID: + @staticmethod + def from_string(s: str) -> ProgramID: ... + @property + def name(self) -> Identifier: ... + @property + def network(self) -> Identifier: ... + def is_aleo(self) -> bool: ... + + +class ProvingKey: + @staticmethod + def from_string(s: str) -> ProvingKey: ... + @staticmethod + def from_bytes(bytes: bytes) -> ProvingKey: ... + def bytes(self) -> bytes: ... + + +class Query: + @staticmethod + def rest(url: str) -> Query: ... + + +class RecordCiphertext: + @staticmethod + def from_string(s: str) -> RecordCiphertext: ... + def decrypt(self, view_key: ViewKey) -> RecordPlaintext: ... + def is_owner(self, view_key: ViewKey) -> bool: ... + + +class RecordPlaintext: + @staticmethod + def from_string(s: str) -> RecordPlaintext: ... + @property + def owner(self) -> str: ... + @property + def nonce(self) -> Group: ... + @property + def version(self) -> int: ... + def serial_number(self, private_key: PrivateKey, program_id: ProgramID, record_identifier: Identifier, record_view_key: Field) -> Field: ... + + +class Response: + @property + def outputs(self) -> list[Value]: ... + + +class Scalar: + @staticmethod + def from_string(s: str) -> Scalar: ... + @staticmethod + def zero() -> Scalar: ... + + +class Signature: + @property + def challenge(self) -> Scalar: ... + @property + def compute_key(self) -> ComputeKey: ... + @property + def response(self) -> Scalar: ... + @staticmethod + def from_string(s: str) -> Signature: ... + @staticmethod + def sign(private_key: PrivateKey, message: bytes) -> Signature: ... + def verify(self, address: Address, message: bytes) -> bool: ... + + +class Trace: + def is_fee(self) -> bool: ... + def is_fee_private(self) -> bool: ... + def is_fee_public(self) -> bool: ... + def transitions(self) -> list[Transition]: ... + def prove_execution(self, locator: str) -> Execution: ... + def prove_fee(self) -> Fee: ... + def prepare(self, query: Query) -> None: ... + + +class Transaction: + @staticmethod + def from_execution(execution: Execution, fee: Fee | None) -> Transaction: ... + @staticmethod + def from_json(json: str) -> Transaction: ... + def to_json(self) -> str: ... + @staticmethod + def from_bytes(bytes: bytes) -> Transaction: ... + def bytes(self) -> bytes: ... + + +class Transition: + @property + def id(self) -> str: ... + @property + def program_id(self) -> ProgramID: ... + @property + def function_name(self) -> Identifier: ... + def is_bond_public(self) -> bool: ... + def is_unbond_public(self) -> bool: ... + def is_fee_private(self) -> bool: ... + def is_fee_public(self) -> bool: ... + def is_split(self) -> bool: ... + @staticmethod + def from_json(json: str) -> Transition: ... + def to_json(self) -> str: ... + @staticmethod + def from_bytes(bytes: bytes) -> Transition: ... + def bytes(self) -> bytes: ... + + +class U8: + def __new__(cls, value: int) -> U8: ... + @staticmethod + def zero() -> U8: ... + def __int__(self) -> int: ... + + +class U16: + def __new__(cls, value: int) -> U16: ... + @staticmethod + def zero() -> U16: ... + def __int__(self) -> int: ... + + +class U32: + def __new__(cls, value: int) -> U32: ... + @staticmethod + def zero() -> U32: ... + def __int__(self) -> int: ... + + +class U64: + def __new__(cls, value: int) -> U64: ... + @staticmethod + def zero() -> U64: ... + def __int__(self) -> int: ... + + +class U128: + def __new__(cls, value: int) -> U128: ... + @staticmethod + def zero() -> U128: ... + def __int__(self) -> int: ... + + +class Value: + @staticmethod + def parse(s: str) -> Value: ... + @staticmethod + def from_literal(literal: Literal) -> Value: ... + @staticmethod + def from_record_plaintext(record_plaintext: RecordPlaintext) -> Value: ... + + +class VerifyingKey: + @staticmethod + def from_string(s: str) -> VerifyingKey: ... + @staticmethod + def from_bytes(bytes: bytes) -> VerifyingKey: ... + def bytes(self) -> bytes: ... + + +class ViewKey: + def decrypt(self, record_ciphertext: RecordCiphertext) -> RecordPlaintext: ... + @staticmethod + def from_string(s: str) -> ViewKey: ... + def is_owner(self, record_ciphertext: RecordCiphertext) -> bool: ... + def to_address(self) -> Address: ... diff --git a/sdk/python/aleo/mainnet.pyi b/sdk/python/aleo/mainnet.pyi new file mode 100644 index 0000000..ecef72c --- /dev/null +++ b/sdk/python/aleo/mainnet.pyi @@ -0,0 +1 @@ +from ._aleolib_mainnet import * # noqa: F401,F403 diff --git a/sdk/python/test.py b/sdk/python/test.py deleted file mode 100644 index 8d22819..0000000 --- a/sdk/python/test.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -import unittest -import aleo - -class TestAleo(unittest.TestCase): - - def test_sanity(self): - c_private_key = "APrivateKey1zkp3dQx4WASWYQVWKkq14v3RoQDfY2kbLssUj7iifi1VUQ6" - c_view_key = "AViewKey1cxguxtKkjYnT9XDza9yTvVMxt6Ckb1Pv4ck1hppMzmCB" - c_address = "aleo184vuwr5u7u0ha5f5k44067dd2uaqewxx6pe5ltha5pv99wvhfqxqv339h4" - - private_key = aleo.PrivateKey.from_string(c_private_key) - - self.assertEqual(str(private_key), c_private_key) - self.assertEqual(str(private_key.view_key()), c_view_key) - self.assertEqual(str(private_key.address()), c_address) - - view_key = aleo.ViewKey.from_string(c_view_key) - self.assertEqual(str(view_key), c_view_key) - self.assertEqual(view_key, private_key.view_key()) - - address = aleo.Address.from_string(c_address) - self.assertEqual(str(address), c_address) - self.assertEqual(address, private_key.address()) - - def test_decrypt_success(self): - c_plaintext = """{ - owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private, - microcredits: 1500000000000000u64.private, - _nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public -}""" - c_ciphertext = "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q" - c_viewkey = "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX" - - view_key = aleo.ViewKey.from_string(c_viewkey) - ciphertext = aleo.RecordCiphertext.from_string(c_ciphertext) - plaintext = view_key.decrypt(ciphertext) - - self.assertEqual(str(plaintext), c_plaintext) - - def test_signature_verify(self): - address = aleo.Address.from_string( - "aleo16u4ecz4yqq0udtnmsy8qzvj8emnua24n27c264f2t3unekdlpy8sh4hat2") - c_signature = "sign1q366eqppwqvmsq0epddmkpqr7ul5rkkltewatf4wdwd82l5yhypdwfnrng6tkj3ryx36wz2dptfq4aev8pwl85u9u6fk48mwmqe35q7h3ptmdtcfxxlcc6ardzayk5ykn2xzp5mhv3spwl3ajgc3y8mfqdmqs7fq3w4wc6j65e3z9ttthqwfy570yef6l9f8klnskzsu9adquzsjwhw" - signature = aleo.Signature.from_string(c_signature) - message = bytes("asd", "utf-8") - bad_message = bytes("qwe", "utf-8") - self.assertTrue(signature.verify(address, message)) - self.assertFalse(signature.verify(address, bad_message)) - self.assertEqual(signature, aleo.Signature.from_string(c_signature)) - - def test_account_sanity(self): - private_key = aleo.PrivateKey.from_string( - "APrivateKey1zkp3dQx4WASWYQVWKkq14v3RoQDfY2kbLssUj7iifi1VUQ6") - account = aleo.Account.from_private_key(private_key) - - self.assertEqual(account.private_key(), private_key) - self.assertEqual(account, aleo.Account.from_private_key(private_key)) - - message = bytes("asd", "utf-8") - bad_message = bytes("qwe", "utf-8") - signature = account.sign(message) - - self.assertTrue(account.verify(signature, message)) - self.assertFalse(account.verify(signature, bad_message)) - self.assertTrue(signature.verify(account.address(), message)) - - def test_encrypt_decrypt_sk(self): - private_key = aleo.PrivateKey.from_string( - "APrivateKey1zkpJYx2NZeJYB74JHpzvQGpKneTP75Dk8dao6paugZXtCz3") - ciphertext = aleo.Ciphertext.from_string( - "ciphertext1qvqt0sp0pp49gjeh50alfalt7ug3g8y7ha6cl3jkavcsnz8d0y9jwr27taxfrwd5kly8lah53qure3vxav6zxr7txattdvscv0kf3vcuqv9cmzj32znx4uwxdawcj3273zhgm8qwpxqczlctuvjvc596mgsqjxwz37f") - recovered = aleo.Encryptor.decrypt_private_key_with_secret(ciphertext, "qwe123") - - self.assertEqual(private_key, recovered) - - encrypted = aleo.Encryptor.encrypt_private_key_with_secret(private_key, "asd123") - other_recovered = aleo.Encryptor.decrypt_private_key_with_secret(encrypted, "asd123") - - self.assertEqual(private_key, other_recovered) - - def test_coinbase(self): - address = aleo.Address.from_string( - "aleo16xwtrvntrfnan84sy3qg2gdkkp5u5p7sjc882lx8n06fjx2k0yqsklw8sv") - solution_json = "{\"partial_solution\":{\"address\":\"aleo16xwtrvntrfnan84sy3qg2gdkkp5u5p7sjc882lx8n06fjx2k0yqsklw8sv\",\"nonce\":5751994693410499959,\"commitment\":\"puzzle163g3gms8kle6z7pfrnelsxmt5qk88sycdxjrfd2chfrmcaa58uv28u4amjhhzyc08wr6ur2hjsusqvgm7mp\"},\"proof.w\":{\"x\":\"46184004058746376929865476153864114989216680475842020861467330568081354981230088442717116178378251337401583339204\",\"y\":\"183283507821413711045927236980084997259573867323884239590264843665205515176450368153011402822680772267880564185790\",\"infinity\":false}}" - challenge_json = "{\"epoch_number\":233,\"epoch_block_hash\":\"ab15lsq2zxsvr0am25afrvnczglagu7utpzuzn2sp94f3vyefm4558quexrn3\",\"degree\":8191}" - challenge = aleo.EpochChallenge.from_json(challenge_json) - solution = aleo.ProverSolution.from_json(solution_json) - - self.assertEqual(solution.address(), address) - self.assertEqual(str(challenge), challenge_json) - self.assertEqual(str(solution), solution_json) - - puzzle = aleo.CoinbasePuzzle.load() - verifying_key = puzzle.verifying_key() - assert solution.verify(verifying_key, challenge, 100) - - def test_transfer(self): - private_key = aleo.PrivateKey.from_string( - "APrivateKey1zkp3dQx4WASWYQVWKkq14v3RoQDfY2kbLssUj7iifi1VUQ6") - destination = aleo.Address.from_string( - "aleo16u4ecz4yqq0udtnmsy8qzvj8emnua24n27c264f2t3unekdlpy8sh4hat2") - amount = aleo.Credits(0.3) - query = aleo.Query.rest("https://explorer.hamp.app") - process = aleo.Process.load() - credits = aleo.Program.credits() - process.add_program(credits) - transfer_name = aleo.Identifier.from_string("transfer_public") - transfer_auth = process.authorize(private_key, credits.id(), transfer_name, [ - aleo.Value.from_literal(aleo.Literal.from_address(destination)), - aleo.Value.from_literal(aleo.Literal.from_u64( - aleo.U64(int(amount.micro())))), - ]) - (_transfer_resp, transfer_trace) = process.execute(transfer_auth) - transfer_trace.prepare(query) - transfer_execution = transfer_trace.prove_execution( - aleo.Locator(credits.id(), aleo.Identifier.from_string("transfer"))) - execution_id = transfer_execution.execution_id() - process.verify_execution(transfer_execution) - - (fee_cost, _) = process.execution_cost(transfer_execution) - fee_priority = None - fee_auth = process.authorize_fee_public( - private_key, fee_cost, execution_id, fee_priority) - (_fee_resp, fee_trace) = process.execute(fee_auth) - fee_trace.prepare(query) - fee = fee_trace.prove_fee() - process.verify_fee(fee, execution_id) - - transaction = aleo.Transaction.from_execution(transfer_execution, fee) - print(transaction.to_json()) - - -if __name__ == "__main__": - unittest.main() From 325e6bc847589b111528206831b83f7b090a3d91 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 12:02:29 -0500 Subject: [PATCH 19/72] =?UTF-8?q?refactor:=20anti-viem=20audit=20=E2=80=94?= =?UTF-8?q?=20properties,=20dunders,=20explicit=20random()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ViewKey: rename #[getter] fn to_address → fn address so Python is vk.address - Stubs: update ViewKey.to_address → @property address in both .pyi files - Stubs: replace Optional[...] with X | None in __init__.pyi; drop unused Optional import - lib.rs: add #![allow(non_local_definitions)] for pyo3 0.20 / newer rustc compat Suite: 9/9 passed; zero aleo-crate compiler warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 9 +++++---- sdk/python/aleo/_aleolib_mainnet.pyi | 3 ++- sdk/src/account/view_key.rs | 2 +- sdk/src/lib.rs | 3 +++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index c432e33..59fa401 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import List, Mapping, Optional, Tuple +from typing import List, Mapping, Tuple class Account: @@ -79,7 +79,7 @@ class Fee: def is_fee_private(self) -> bool: ... def is_fee_public(self) -> bool: ... @property - def payer(self) -> Optional[Address]: ... + def payer(self) -> Address | None: ... @property def transition(self) -> Transition: ... @staticmethod @@ -374,7 +374,7 @@ class Trace: class Transaction: @staticmethod - def from_execution(execution: Execution, fee: Optional[Fee]) -> Transaction: ... + def from_execution(execution: Execution, fee: Fee | None) -> Transaction: ... @staticmethod def from_json(json: str) -> Transaction: ... def to_json(self) -> str: ... @@ -460,4 +460,5 @@ class ViewKey: @staticmethod def from_string(s: str) -> ViewKey: ... def is_owner(self, record_ciphertext: RecordCiphertext) -> bool: ... - def to_address(self) -> Address: ... + @property + def address(self) -> Address: ... diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index 6594fb3..b910be4 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -454,4 +454,5 @@ class ViewKey: @staticmethod def from_string(s: str) -> ViewKey: ... def is_owner(self, record_ciphertext: RecordCiphertext) -> bool: ... - def to_address(self) -> Address: ... + @property + def address(self) -> Address: ... diff --git a/sdk/src/account/view_key.rs b/sdk/src/account/view_key.rs index 002d19c..581280f 100644 --- a/sdk/src/account/view_key.rs +++ b/sdk/src/account/view_key.rs @@ -50,7 +50,7 @@ impl ViewKey { /// Returns the address corresponding to the view key. #[getter] - fn to_address(&self) -> Address { + fn address(&self) -> Address { self.0.to_address().into() } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 293e7cb..1dffadd 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -1,3 +1,6 @@ +// pyo3 0.20 macro expansion triggers this lint on newer rustc; remove after pyo3 >= 0.21 upgrade +#![allow(non_local_definitions)] + // Copyright (C) 2019-2023 Aleo Systems Inc. // This file is part of the Aleo SDK library. From 01a44a56865fdba047cb171318a2c733bd36d7e4 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 12:04:46 -0500 Subject: [PATCH 20/72] docs: update usage for aleo.mainnet + snarkvm 4.7.3 --- README.md | 20 +++++++++++++++++++- sdk/Readme.md | 40 ++++++++++++++++++++++------------------ sdk/build.sh | 5 ++--- sdk/install.sh | 9 ++++----- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b7896e7..330a5eb 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,27 @@ Welcome to the Aleo Python SDK! This SDK provides a set of libraries aimed at empowering Python developers with zk (zero-knowledge) capabilities. +## Quick Start + +```python +from aleo.mainnet import PrivateKey, Signature + +# Generate a random private key +pk = PrivateKey.random() +print(f"Address: {pk.address}") +print(f"View Key: {pk.view_key}") + +# Sign and verify a message +message = b"hello" +sig = Signature.sign(pk, message) +assert sig.verify(pk.address, message) +``` + +Built with snarkvm 4.7.3 (MainnetV0). For build instructions, see [sdk/Readme.md](./sdk/Readme.md). + ## Codebases Included -- [**sdk**](./sdk/): A library that brings Aleo functionalities to Python developers. +- [**sdk**](./sdk/): A library that brings Aleo MainnetV0 functionalities to Python developers. - [**zkml**](./zkml/): A transpiler library that converts Python machine learning models into Leo code. - [**zkml-research**](./zkml-research/): Research on accurate/constraint-efficient zkML techniques, mostly for internal purposes. diff --git a/sdk/Readme.md b/sdk/Readme.md index a655cb1..3509441 100644 --- a/sdk/Readme.md +++ b/sdk/Readme.md @@ -1,26 +1,30 @@ -# Aleo Python SDK +# Aleo Python SDK (MainnetV0) -The Aleo Python SDK provides a set of libraries aimed at empowering Python developers with zk (zero-knowledge) -programming capabilities via the usage of Aleo's zkSnarks. +The Aleo Python SDK provides Python bindings to Aleo's zero-knowledge cryptographic primitives, built with snarkvm 4.7.3. -Currently this SDK is in alpha preview stage. It can be installed by using the following command: - ```bash - pip3 install zkml - ``` +## Quick Start + +```python +from aleo.mainnet import PrivateKey, Signature + +# Generate a random private key +pk = PrivateKey.random() +address = pk.address +view_key = pk.view_key + +# Sign a message +message = b"hello" +signature = Signature.sign(pk, message) +assert signature.verify(address, message) +``` + +## Build & Install -Alternatively, you can clone it from [GitHub](https://github.com/AleoHQ/python-sdk/tree/master/sdk) and run: ```bash bash install.sh ``` -This will print out a simple aleo Private Key that is generated by the entropy present on your device. - -## Contributing -If you wish to contribute, please follow the contribution guidelines outlined on [GitHub](https://github.com/AleoHQ/python-sdk/blob/master/sdk/CONTRIBUTING.md). -## Future Work Planned -This SDK will be expanded to include the following features: -* Aleo Account Management -* Aleo program deployment, execution, and management -* Leo program compilation and execution +This creates a development environment and installs the `aleo` package with MainnetV0 bindings. -It is also planned to integrate this SDK with Aleo's ZkML Package so that the full suite of Aleo program execution and account management capabilities can be used with ZkML. \ No newline at end of file +## Contributing +If you wish to contribute, please follow the contribution guidelines outlined on [GitHub](https://github.com/AleoHQ/python-sdk/blob/master/sdk/CONTRIBUTING.md). \ No newline at end of file diff --git a/sdk/build.sh b/sdk/build.sh index 1a1ec90..c2cd9bd 100755 --- a/sdk/build.sh +++ b/sdk/build.sh @@ -1,9 +1,8 @@ #!/bin/bash ## This script assumes you have already installed Rust 1.6+ and Python 3.8.5+ -## The script below will install the Aleo Python bindings and create the aleo_python library allowing you to use -## the Aleo implementation of the Poseidon hash function in Python. +## The script below will install the Aleo Python bindings and create the aleo library with MainnetV0 support. python3 -m venv .env source .env/bin/activate pip install maturin -maturin build +maturin build --features mainnet diff --git a/sdk/install.sh b/sdk/install.sh index 8532aff..9960917 100755 --- a/sdk/install.sh +++ b/sdk/install.sh @@ -1,10 +1,9 @@ #!/bin/bash ## This script assumes you have already installed Rust 1.6+ and Python 3.8.5+ -## The script below will install the Aleo Python bindings and create the aleo_python library allowing you to use -## the Aleo implementation of the Poseidon hash function in Python. +## The script below will install the Aleo Python bindings and create the aleo library with MainnetV0 support. python3 -m venv .env source .env/bin/activate -pip install maturin -maturin develop -python python/test.py +pip install maturin pytest +maturin develop --features mainnet +python -m pytest python/tests -v From 3ae5ac11dbc03706bb7fe715b740d2e320957640 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 12:18:37 -0500 Subject: [PATCH 21/72] fix: repair Encryptor property access, correct execution_cost stubs, add encryptor tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/Cargo.toml | 3 +++ sdk/python/aleo/__init__.pyi | 2 +- sdk/python/aleo/_aleolib_mainnet.pyi | 9 +-------- sdk/python/aleo/encryptor.py | 2 +- sdk/python/tests/test_encryptor.py | 26 ++++++++++++++++++++++++++ 5 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 sdk/python/tests/test_encryptor.py diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 02aad35..3db36e5 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" license = "GPL-3.0-or-later" [lib] +# NOTE: internal crate name only. The importable module name is set by +# pyproject.toml [tool.maturin] module-name + the cfg-gated #[pyo3(name)] +# in src/lib.rs (_aleolib_mainnet / _aleolib_testnet). name = "_aleolib" crate-type = ["cdylib"] diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index 59fa401..cb43e37 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -275,7 +275,7 @@ class Process: def execute(self, authorization: Authorization) -> Tuple[Response, Trace]: ... def verify_execution(self, execution: Execution) -> None: ... def verify_fee(self, fee: Fee, deployment_or_execution_id: Field) -> None: ... - def execution_cost(self, execution: Execution) -> Tuple[MicroCredits, Tuple[MicroCredits, MicroCredits]]: ... + def execution_cost(self, execution: Execution) -> tuple[int, tuple[int, int]]: ... class Program: diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index b910be4..656f72d 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -55,13 +55,6 @@ class ComputeKey: def sk_prf(self) -> Scalar: ... -class Encryptor: - @staticmethod - def encrypt_private_key_with_secret(private_key: PrivateKey, secret: str) -> Ciphertext: ... - @staticmethod - def decrypt_private_key_with_secret(ciphertext: Ciphertext, secret: str) -> PrivateKey: ... - - class Execution: @property def execution_id(self) -> Field: ... @@ -270,7 +263,7 @@ class Process: def execute(self, authorization: Authorization) -> tuple[Response, Trace]: ... def verify_execution(self, execution: Execution) -> None: ... def verify_fee(self, fee: Fee, deployment_or_execution_id: Field) -> None: ... - def execution_cost(self, execution: Execution) -> tuple[MicroCredits, tuple[MicroCredits, MicroCredits]]: ... + def execution_cost(self, execution: Execution) -> tuple[int, tuple[int, int]]: ... class Program: diff --git a/sdk/python/aleo/encryptor.py b/sdk/python/aleo/encryptor.py index 01b3fe2..35c6abe 100644 --- a/sdk/python/aleo/encryptor.py +++ b/sdk/python/aleo/encryptor.py @@ -10,7 +10,7 @@ class Encryptor: def encrypt_private_key_with_secret(private_key: PrivateKey, secret: str) -> Ciphertext: """Encrypts a private key into ciphertext using a secret. """ - seed = private_key.seed() + seed = private_key.seed return Encryptor.__encrypt_field(seed, secret, "private_key") @staticmethod diff --git a/sdk/python/tests/test_encryptor.py b/sdk/python/tests/test_encryptor.py new file mode 100644 index 0000000..ed49500 --- /dev/null +++ b/sdk/python/tests/test_encryptor.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import pytest +from aleo.mainnet import PrivateKey +from aleo.encryptor import Encryptor + + +def test_encrypt_decrypt_private_key_roundtrip(): + pk = PrivateKey.random() + ct = Encryptor.encrypt_private_key_with_secret(pk, "s3cret") + recovered = Encryptor.decrypt_private_key_with_secret(ct, "s3cret") + assert recovered == pk + + +def test_wrong_secret_fails(): + pk = PrivateKey.random() + ct = Encryptor.encrypt_private_key_with_secret(pk, "s3cret") + # A wrong secret produces a bad blinding factor; the recovered seed will be + # incorrect, yielding either a raised exception (crypto/assertion failure) + # or a different private key. Either outcome is acceptable — we just verify + # that the wrong-secret result is NOT equal to the original key. + try: + recovered = Encryptor.decrypt_private_key_with_secret(ct, "wrong") + assert recovered != pk, "wrong secret must not decrypt to the original key" + except Exception: + pass # any exception is also a correct rejection From b11ec133dc27a7192eb70c3140d447d4fb403271 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 12:21:09 -0500 Subject: [PATCH 22/72] test: make wrong-secret encryptor test fail on unexpected exception types Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- sdk/python/tests/test_encryptor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/python/tests/test_encryptor.py b/sdk/python/tests/test_encryptor.py index ed49500..8e38af7 100644 --- a/sdk/python/tests/test_encryptor.py +++ b/sdk/python/tests/test_encryptor.py @@ -16,11 +16,11 @@ def test_wrong_secret_fails(): pk = PrivateKey.random() ct = Encryptor.encrypt_private_key_with_secret(pk, "s3cret") # A wrong secret produces a bad blinding factor; the recovered seed will be - # incorrect, yielding either a raised exception (crypto/assertion failure) - # or a different private key. Either outcome is acceptable — we just verify - # that the wrong-secret result is NOT equal to the original key. + # incorrect, yielding either a ValueError/RuntimeError from the crypto layer + # (an outright rejection) or a different private key. AssertionError and + # unexpected exception types (e.g. TypeError from an API break) must FAIL. try: recovered = Encryptor.decrypt_private_key_with_secret(ct, "wrong") - assert recovered != pk, "wrong secret must not decrypt to the original key" - except Exception: - pass # any exception is also a correct rejection + except (ValueError, RuntimeError): + return # crypto layer rejected the wrong secret outright — acceptable + assert recovered != pk, "wrong secret must not decrypt to the original key" From cec90075a0e932d1aa12ad1ac84d94c46f9b51a4 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 13:09:10 -0500 Subject: [PATCH 23/72] build: upgrade snarkvm to git tag v4.8.1 - snarkvm moves from crates.io 4.7.3 to git tag v4.8.1 (not yet published) - ConsensusVersion pins bumped V15 -> V17 (4.8.x added V16/V17) in verify_execution, verify_fee, and execution_cost - add .cargo/config.toml so plain cargo builds link pyo3 extension modules on macOS (maturin injects these flags itself) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- sdk/.cargo/config.toml | 6 ++ sdk/Cargo.lock | 178 ++++++++++++------------------------ sdk/Cargo.toml | 2 +- sdk/src/programs/process.rs | 6 +- 4 files changed, 71 insertions(+), 121 deletions(-) create mode 100644 sdk/.cargo/config.toml diff --git a/sdk/.cargo/config.toml b/sdk/.cargo/config.toml new file mode 100644 index 0000000..1d1292d --- /dev/null +++ b/sdk/.cargo/config.toml @@ -0,0 +1,6 @@ +# PyO3 extension modules must leave Python symbols undefined at link time +# (they resolve against the host interpreter at import). maturin injects +# these flags itself; this config makes plain `cargo build`/`cargo test` +# link successfully on macOS too (e.g. the --features testnet check). +[target.'cfg(target_os = "macos")'] +rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index 6345d29..f2ca0c9 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -2289,9 +2289,8 @@ dependencies = [ [[package]] name = "snarkvm" -version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2051aaa63e873040259b906bfdf0e196b7fad78548bf117d50b0677fa9cc5f6" +version = "4.8.1" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "rand", @@ -2308,8 +2307,7 @@ dependencies = [ [[package]] name = "snarkvm-algorithms" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb463ee0dbf27520e3b885239da9539d1817bef54414e166abecf53e89bd9182" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", @@ -2336,8 +2334,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb79631afb088804f48b2b5c553d80003d2d29d164ebdd72b4ae2805167dd294" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2351,8 +2348,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-account" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec5f1223fb704743fb493b810ff7827f97b14ba6522b2a30863c7ed9b8c419" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-network", "snarkvm-circuit-types", @@ -2362,8 +2358,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-algorithms" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbbdf99a4b98096477aa642e87f3c032de69dd312120c71557bd3715f7a6ca7" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-types", "snarkvm-console-algorithms", @@ -2373,8 +2368,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-collections" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d84b57411579eddf10b9c844874188fa6a1ef461dd195e1004fc0b8b80b8851" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-types", @@ -2384,8 +2378,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab81d401f337875ebfd9a33551b4ac81e2c79d70309b37e661551df4e756c8ab" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "indexmap", @@ -2405,14 +2398,12 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment-witness" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19c9e7e5832d167a90d02f2a3dbb329a35ac434d828dfa5a303201ee98f20458" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" [[package]] name = "snarkvm-circuit-network" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f3d2413bc19683240cfb561417b90fd45dd7104c82318dae5aa6f4789e49bf" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-collections", @@ -2423,8 +2414,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-program" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336325d1cb446c27a0e519c46671efb4d0afeae5509de142b5225741ad34f6f9" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2438,8 +2428,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51eb87b289e03b4fa2d6d1d2ae1a731e2471c19916690bd3881e838e5c5f2a0c" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-address", @@ -2454,8 +2443,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-address" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67f19e8cc472228e6d502c194ee0d40ac8e55f6d6796c4a9de19867f8790310" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2468,8 +2456,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-boolean" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62da3b39df237d0bc23db92a77356ae9d78e3030254a777132f7ffa1e361cfaa" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-console-types-boolean", @@ -2478,8 +2465,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-field" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d791b3f4c37906ebe8dc22f4e9b0596e7d3498619f8654da631cc1494f9ad5dc" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2489,8 +2475,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-group" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ac8d680858600c493c592cac496fb0f6ae3542845d79cdf71059dbe4189f8b3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2502,8 +2487,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-integers" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3ca8f3eae95acb4484f3e1b23dab4a979e10db8fc1dadd8f156413ce8303fa" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2515,8 +2499,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-scalar" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875d94a9ebdb15b18f0141a6fd1abfc08d7c4268d08b2729d9343dd7e53557c1" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2527,8 +2510,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-string" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e2f1ffdf6cc1bedb1d0e97388096e17a9376ff387f787172caaa1a163642d" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2540,8 +2522,7 @@ dependencies = [ [[package]] name = "snarkvm-console" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0471631ca8ca82c66a21a41dc83691ac949423564eeb67eb0b84a29e9d6b3983" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-account", "snarkvm-console-algorithms", @@ -2554,8 +2535,7 @@ dependencies = [ [[package]] name = "snarkvm-console-account" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "752db19fec19734761c805077da1e80e3ef5e68fac0ab94ebbf2c67559780fce" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "bs58", "snarkvm-console-network", @@ -2566,8 +2546,7 @@ dependencies = [ [[package]] name = "snarkvm-console-algorithms" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeeab6a2a88ecff12d1f146bd652ad4445dcd2fa4f429235e50a7b634fa08927" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "blake2s_simd", "hex", @@ -2583,8 +2562,7 @@ dependencies = [ [[package]] name = "snarkvm-console-collections" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60bd7a8bb5498077596820b1659296d09bb47165d14512d8e6a77bb37c2e7d4c" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "parking_lot", @@ -2597,8 +2575,7 @@ dependencies = [ [[package]] name = "snarkvm-console-network" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d9820c26fc45b4ecc4428363d865679a06bbaf20f16d243989558dc4f3a6b6a" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "enum-iterator", @@ -2618,8 +2595,7 @@ dependencies = [ [[package]] name = "snarkvm-console-network-environment" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a68a723c76f588353bfc6c32d23784a93afbdde70232eece87721e3b9667718" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "bech32", @@ -2637,8 +2613,7 @@ dependencies = [ [[package]] name = "snarkvm-console-program" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e08eea78d9bd65da1b6500a03c5eb46fca629598449f2e5f8ddf7f6c5b497d9" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "enum-iterator", "enum_index", @@ -2659,8 +2634,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c1d0cc78b332ab23e3caecd78384cba7739588fc347d59e1bc4b8f6781c938" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-address", @@ -2675,8 +2649,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-address" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e784ce5f6ae527e47daa30257bfbc03a6faa07a58b56980d50096bdb17cdf4" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2687,8 +2660,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-boolean" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37774ccb2e135e81210a00b2ac7a7e831bee8dc77f705b4dd1400784fffb1cb" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", ] @@ -2696,8 +2668,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-field" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fdb611ced23d55453a6607aa1f5742c21791e320c53173f9b6c89cc99915fc" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2707,8 +2678,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-group" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adab0b7f3d8e906be46d4a4a705401d179d75b6c7c507a54e1ac21794d58d7f6" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2719,8 +2689,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-integers" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce77a6d7ad511e10cbf15e413278f7c55cc98cd9f8cc97bf9a2a97949a356e8" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2731,8 +2700,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-scalar" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26872058a807c86fc29be004db89d06061063bf0669cede9598153948a46db18" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2743,8 +2711,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-string" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e98c7497bb3861a1610a6987edf5b3e79b76c249209252baeb1767a8e238e51" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2755,8 +2722,7 @@ dependencies = [ [[package]] name = "snarkvm-curves" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb85f50dc014ca37d78e91187e3ed6f2c5f36c505656841647c8a7d16d3951e" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "rand", "rustc_version", @@ -2769,8 +2735,7 @@ dependencies = [ [[package]] name = "snarkvm-fields" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a60e65f8e1c8be65456815cb016cf9c153f272948ebce50f01c5c7b9832eb5c" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", @@ -2787,11 +2752,11 @@ dependencies = [ [[package]] name = "snarkvm-ledger" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d090a5d10c85218c8f302b7971ac26266fcf395ec60a7946d6be202585314e" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", + "cfg-if", "indexmap", "lru", "parking_lot", @@ -2817,8 +2782,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-authority" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca088fc9fbab4f64484f016c7b8f43ac7864e38962776ba5fe87e340b8a9d1d0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "rand", @@ -2830,8 +2794,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-block" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab7feccfa232a8125d5d08523f1097c2ff9f940df1b5dd680db33e22051b282" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "indexmap", @@ -2855,8 +2818,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-committee" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b4eb3a3f68f644f3a97badd97231a6142be4c544a827d33527de8bdf0a63b" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "indexmap", "rayon", @@ -2868,8 +2830,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e1fec476440f0ab21e9f4cb7de4ca0599e15fd96f4e2c36e5cde5cdeb0fd05c" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-ledger-narwhal-batch-certificate", "snarkvm-ledger-narwhal-batch-header", @@ -2882,8 +2843,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-certificate" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2820939e81acdb3a1e956c02a1519a697c04c42500369e13334175c1be5a474c" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "indexmap", "rayon", @@ -2896,8 +2856,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-header" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc1aa3d423f36e151fded47358648b3e9f5bdef8f14b649867b2970a8b478f0" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "indexmap", "rayon", @@ -2909,8 +2868,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-data" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c221e93a87d8d95d04a7ae80cbc002f6f234de6b0aeb2e4cc671472c4ed86a" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "bytes", "serde_json", @@ -2921,8 +2879,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-subdag" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909c86ab8ffb66d68e65008a2bc7587f804f90c880e546959e012768f2f52c8a" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "indexmap", "rayon", @@ -2937,8 +2894,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e016abadbac845bcd8a07ed7ac5838aa770017c0fc0cc48f69e21b3b9b21b" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "bytes", "serde_json", @@ -2951,8 +2907,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission-id" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90735fbb67ea6a28835fa4aca7fbd23f8082254001a26458fd54bd8e25a280ec" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "snarkvm-console", "snarkvm-ledger-puzzle", @@ -2961,8 +2916,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9930f2b8f6d7af1c4df5ca9e1fc58a1245614d602a23dcacdba2f49c196dce" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", @@ -2982,8 +2936,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle-epoch" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39769888aa99f476ba96f58f130baec1adf8cc2cb403e582f6b750d9fe7b4618" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", @@ -3005,8 +2958,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-query" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fab416cca1cc120da5e23da9f96c33e63a66291d5b57900edff4e066eb37860" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "async-trait", @@ -3023,8 +2975,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-store" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af053ce8e70479bf733f1060d8583f5508fe395afe1776494428dd8bd3d6b198" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std-storage", "anyhow", @@ -3049,8 +3000,7 @@ dependencies = [ [[package]] name = "snarkvm-parameters" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce50be3cbc636838eda13c6312fe1c957b3d09a61fe8d1636f2ce227be756e60" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", @@ -3072,8 +3022,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef3f5387331260f4af6c6ce339814516e5783f6c78ec83cceb5a922688e25c7" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", @@ -3087,6 +3036,7 @@ dependencies = [ "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", + "snarkvm-ledger-authority", "snarkvm-ledger-block", "snarkvm-ledger-committee", "snarkvm-ledger-narwhal-data", @@ -3107,8 +3057,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-error" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f292aa7ab9d8db2a092256cf79ee704445b82a0bfdc008ddce2fde78bf5f56" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "anyhow", "snarkvm-circuit-environment", @@ -3120,8 +3069,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-process" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e9a83a46498378477aaee8e7a07a5ddd2cd5a7c05bf9b2a1497cda317ac3dc" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "colored", @@ -3147,8 +3095,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-program" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22fa0ec46f528d531af6ab72005a51acff64d7c9a9732384f11c59c65f64f50" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "enum-iterator", "indexmap", @@ -3169,8 +3116,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-snark" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef07277ee50c309a3e74d49d869ffc5e4e358a00b57738760cdb2a8e2ab77ebe" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "bincode", "serde_json", @@ -3183,8 +3129,7 @@ dependencies = [ [[package]] name = "snarkvm-utilities" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4d04f7357366dcbbae21249334ed2ad5ffefc09bd00f0d418f4bba34fba86c" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "aleo-std", "anyhow", @@ -3207,8 +3152,7 @@ dependencies = [ [[package]] name = "snarkvm-utilities-derives" version = "4.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50ca261adb518781bf4cfb9d6b69c385d00fca93bf5b5e6838c93598206bd8" +source = "git+https://github.com/ProvableHQ/snarkVM.git?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" dependencies = [ "proc-macro2", "quote 1.0.46", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 3db36e5..84c2abb 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -28,6 +28,6 @@ rand = { version = "0.10" } serde = "1" serde_json = "1" -snarkvm = { version = "4.7.3", default-features = false, features = [ +snarkvm = { git = "https://github.com/ProvableHQ/snarkVM.git", tag = "v4.8.1", default-features = false, features = [ "console", "circuit", "synthesizer", "ledger", "utilities", "algorithms", "parameters", ] } diff --git a/sdk/src/programs/process.rs b/sdk/src/programs/process.rs index 5d27760..99fe4bd 100644 --- a/sdk/src/programs/process.rs +++ b/sdk/src/programs/process.rs @@ -179,7 +179,7 @@ impl Process { }) .collect::<anyhow::Result<IndexMap<_, _>>>()?; ProcessNative::verify_execution( - ConsensusVersion::V15, + ConsensusVersion::V17, VarunaVersion::V2, InclusionVersion::V1, &execution, @@ -190,7 +190,7 @@ impl Process { /// Verifies the given fee is valid. Note: This does not check that the global state root exists in the ledger. fn verify_fee(&self, fee: &Fee, deployment_or_execution_id: Field) -> anyhow::Result<()> { self.0.verify_fee( - ConsensusVersion::V15, + ConsensusVersion::V17, VarunaVersion::V2, InclusionVersion::V1, fee, @@ -200,6 +200,6 @@ impl Process { /// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). fn execution_cost(&self, execution: &Execution) -> anyhow::Result<(u64, (u64, u64))> { - execution_cost(&self.0, &execution.clone().into(), ConsensusVersion::V15) + execution_cost(&self.0, &execution.clone().into(), ConsensusVersion::V17) } } From c81073c141ecb0ebe6419f16b191229f466b6086 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 13:34:04 -0500 Subject: [PATCH 24/72] test: cover execute/prove/verify, fee authorization, cost, transaction assembly, serial numbers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .circleci/config.yml | 3 +- sdk/pytest.ini | 2 + sdk/python/tests/test_proving.py | 199 ++++++++++++++++++++++++++ sdk/python/tests/vectors/records.json | 4 + 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 sdk/python/tests/test_proving.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 8d268e7..bdd59bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,8 @@ jobs: command: | . .env/bin/activate pip install pytest - python -m pytest python/tests -v + # slow proving tests: run locally with -m slow + python -m pytest python/tests -v -m "not slow" workflows: version: 2 diff --git a/sdk/pytest.ini b/sdk/pytest.ini index ae50458..f08e011 100644 --- a/sdk/pytest.ini +++ b/sdk/pytest.ini @@ -1,3 +1,5 @@ [pytest] testpaths = python/tests python_files = test_*.py +markers = + slow: proving tests that need network access and SNARK parameter downloads (run with -m slow) diff --git a/sdk/python/tests/test_proving.py b/sdk/python/tests/test_proving.py new file mode 100644 index 0000000..bcc8fa4 --- /dev/null +++ b/sdk/python/tests/test_proving.py @@ -0,0 +1,199 @@ +"""End-to-end tests for the execute/prove/verify/fee/cost/transaction paths. + +Fast tests (unmarked) are fully offline: `Process.execute(auth)` synthesizes the +circuit locally without a proof, and fee authorization / serial numbers are pure +local computation. (First-ever run downloads a small ~3 MB SRS chunk to ~/.aleo.) + +Slow tests (`@pytest.mark.slow`) require: + * network access to the mainnet REST endpoint (for `trace.prepare`, which + fetches the latest global state root), and + * SNARK proving parameter downloads on first use (hundreds of MB, cached in + ~/.aleo/resources) — the first run can take several minutes. +Run them locally with: python -m pytest python/tests -v -m slow +They are excluded from CI via -m "not slow". + +Endpoint note (verified empirically): Query.rest() wants the BASE url +`https://api.explorer.provable.com/v1` — snarkvm's REST query appends the +network path (`/mainnet/stateRoot/latest`) itself; passing `.../v1/mainnet` +would double the network segment. +""" + +import json + +import pytest + +from aleo.mainnet import ( + Authorization, + Execution, + Field, + Identifier, + Locator, + PrivateKey, + Process, + ProgramID, + Query, + RecordCiphertext, + Transaction, + Value, + ViewKey, +) +from conftest import load_vectors + +ENDPOINT = "https://api.explorer.provable.com/v1" + +CREDITS = ProgramID.from_string("credits.aleo") +TRANSFER_PUBLIC = Identifier.from_string("transfer_public") + + +def _authorize_transfer_public(process: Process, private_key: PrivateKey) -> Authorization: + recipient = str(private_key.address) + return process.authorize( + private_key, + CREDITS, + TRANSFER_PUBLIC, + [Value.parse(recipient), Value.parse("10u64")], + ) + + +# --------------------------------------------------------------------------- +# Fast / offline tests +# --------------------------------------------------------------------------- + + +def test_execute_transfer_public(): + process = Process.load() + pk = PrivateKey.random() + auth = _authorize_transfer_public(process, pk) + + response, trace = process.execute(auth) + + outputs = response.outputs + assert isinstance(outputs, list) + assert len(outputs) == 1 # transfer_public returns a single future + assert "transfer_public" in str(outputs[0]) + + transitions = trace.transitions() + assert len(transitions) == 1 + assert str(transitions[0].program_id) == "credits.aleo" + assert str(transitions[0].function_name) == "transfer_public" + assert trace.is_fee() is False + + +def test_authorize_fee_public(): + process = Process.load() + pk = PrivateKey.random() + + fee_auth = process.authorize_fee_public(pk, 1000, 0, Field.zero()) + assert fee_auth is not None + + # JSON round-trip + round_tripped = Authorization.from_json(fee_auth.to_json()) + assert str(round_tripped) == str(fee_auth) + + +def test_serial_number_deterministic(): + v = load_vectors("records.json")["decrypt_kat"] + + plaintext = RecordCiphertext.from_string(v["ciphertext"]).decrypt( + ViewKey.from_string(v["view_key"]) + ) + owner_key = PrivateKey.from_string(v["private_key"]) + # Sanity: the vendored private key really owns the KAT record. + assert str(owner_key.address) == v["owner"] + + record_view_key = Field.from_string(v["record_view_key"]) + args = (owner_key, CREDITS, Identifier.from_string("credits"), record_view_key) + + sn1 = plaintext.serial_number(*args) + sn2 = plaintext.serial_number(*args) + + assert str(sn1) == str(sn2) # deterministic + assert str(sn1).endswith("field") + + +# --------------------------------------------------------------------------- +# Slow / network tests (proving) +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def process(): + return Process.load() + + +@pytest.fixture(scope="module") +def signer(): + return PrivateKey.random() + + +@pytest.fixture(scope="module") +def proven_execution(process, signer): + """Authorize + execute + prove a credits.aleo/transfer_public execution. + + Module-scoped so the expensive proof (and first-run parameter downloads, + which can take minutes) happens exactly once across the slow tests. + """ + auth = _authorize_transfer_public(process, signer) + _, trace = process.execute(auth) + trace.prepare(Query.rest(ENDPOINT)) + # NOTE: despite the .pyi stub saying `locator: str`, the native binding + # requires a Locator object; a plain str raises TypeError. + return trace.prove_execution(Locator.from_string("credits.aleo/transfer_public")) + + +@pytest.mark.slow +def test_prove_and_verify_execution(process, proven_execution): + execution = proven_execution + + # verify_execution raises on failure; returning None means success. + assert process.verify_execution(execution) is None + + # Cost: (total, (storage, finalize)) in microcredits. + total, (storage, finalize) = process.execution_cost(execution) + assert total > 0 + assert storage > 0 + assert finalize >= 0 + assert total == storage + finalize + + # Negative: tamper with the signer commitment (scm). Determined + # empirically: tampering tcm/inputs/outputs is rejected already at + # Execution.from_json ("Transition ID mismatch"), because the transition + # id is recomputed on deserialization — but scm is not part of the + # transition id, so the tampered execution parses fine and must then fail + # proof verification. + tampered = json.loads(execution.to_json()) + assert tampered["transitions"][0]["scm"] != "0field" + tampered["transitions"][0]["scm"] = "0field" + tampered_execution = Execution.from_json(json.dumps(tampered)) + with pytest.raises(Exception): + process.verify_execution(tampered_execution) + + +@pytest.mark.slow +def test_prove_fee_and_transaction(process, signer, proven_execution): + execution = proven_execution + execution_id = execution.execution_id + + # Authorize and prove a public fee bound to this execution id. + fee_auth = process.authorize_fee_public(signer, 1_000_000, 0, execution_id) + _, fee_trace = process.execute(fee_auth) + assert fee_trace.is_fee() is True + assert fee_trace.is_fee_public() is True + + fee_trace.prepare(Query.rest(ENDPOINT)) + fee = fee_trace.prove_fee() + assert fee.is_fee_public() is True + assert str(fee.payer) == str(signer.address) + + # verify_fee raises on failure; it must be bound to the same execution id. + assert process.verify_fee(fee, execution_id) is None + with pytest.raises(Exception): + process.verify_fee(fee, Field.zero()) + + # Assemble a transaction and JSON round-trip it. + tx = Transaction.from_execution(execution, fee) + tx_json = tx.to_json() + parsed = json.loads(tx_json) + assert parsed["type"] == "execute" + round_tripped = Transaction.from_json(tx_json) + assert round_tripped.to_json() == tx_json diff --git a/sdk/python/tests/vectors/records.json b/sdk/python/tests/vectors/records.json index eb4f3bc..4f721e6 100644 --- a/sdk/python/tests/vectors/records.json +++ b/sdk/python/tests/vectors/records.json @@ -1,7 +1,11 @@ { "_source": "github.com/ProvableHQ/sdk@543b41e0a7e3d6a8a49ae8d0870809dc92b88684", "decrypt_kat": { + "_private_key_source": "owning private key for view_key below, vendored from ProvableHQ/sdk@543b41e test constants", + "private_key": "APrivateKey1zkpJkyYRGYtkeHDaFfwsKtUJzia7csiWhfBWPXWhXJzy9Ls", "view_key": "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX", + "_record_view_key_source": "ProvableHQ/sdk@543b41e wasm/src/record/record_ciphertext.rs:224", + "record_view_key": "4445718830394614891114647247073357094867447866913203502139893824059966201724field", "non_owner_view_key": "AViewKey1e2WyreaH5H4RBcioLL2GnxvHk5Ud46EtwycnhTdXLmXp", "ciphertext": "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q", "owner": "aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3", From 97d63a0ea8137f436820a61d8151afdddc571635 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 13:39:50 -0500 Subject: [PATCH 25/72] fix: prove_execution takes a locator string, matching native API and stubs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_proving.py | 9 +++------ sdk/src/programs/trace.rs | 10 ++++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/sdk/python/tests/test_proving.py b/sdk/python/tests/test_proving.py index bcc8fa4..e130619 100644 --- a/sdk/python/tests/test_proving.py +++ b/sdk/python/tests/test_proving.py @@ -27,7 +27,6 @@ Execution, Field, Identifier, - Locator, PrivateKey, Process, ProgramID, @@ -136,9 +135,7 @@ def proven_execution(process, signer): auth = _authorize_transfer_public(process, signer) _, trace = process.execute(auth) trace.prepare(Query.rest(ENDPOINT)) - # NOTE: despite the .pyi stub saying `locator: str`, the native binding - # requires a Locator object; a plain str raises TypeError. - return trace.prove_execution(Locator.from_string("credits.aleo/transfer_public")) + return trace.prove_execution("credits.aleo/transfer_public") @pytest.mark.slow @@ -165,7 +162,7 @@ def test_prove_and_verify_execution(process, proven_execution): assert tampered["transitions"][0]["scm"] != "0field" tampered["transitions"][0]["scm"] = "0field" tampered_execution = Execution.from_json(json.dumps(tampered)) - with pytest.raises(Exception): + with pytest.raises(RuntimeError): process.verify_execution(tampered_execution) @@ -187,7 +184,7 @@ def test_prove_fee_and_transaction(process, signer, proven_execution): # verify_fee raises on failure; it must be bound to the same execution id. assert process.verify_fee(fee, execution_id) is None - with pytest.raises(Exception): + with pytest.raises(RuntimeError): process.verify_fee(fee, Field.zero()) # Assemble a transaction and JSON round-trip it. diff --git a/sdk/src/programs/trace.rs b/sdk/src/programs/trace.rs index 370e64f..2f59bae 100644 --- a/sdk/src/programs/trace.rs +++ b/sdk/src/programs/trace.rs @@ -15,8 +15,8 @@ // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. use crate::{ - types::{CurrentAleo, LocatorNative, QueryNative, TraceNative}, - Execution, Fee, Locator, Query, Transition, + types::{CurrentAleo, QueryNative, TraceNative}, + Execution, Fee, Query, Transition, }; use pyo3::prelude::*; @@ -55,12 +55,10 @@ impl Trace { } /// Returns a new execution with a proof, for the current inclusion assignments and global state root. - fn prove_execution(&self, locator: Locator) -> anyhow::Result<Execution> { - let locator: LocatorNative = locator.into(); - let locator_s = locator.to_string(); + fn prove_execution(&self, locator: &str) -> anyhow::Result<Execution> { self.0 .prove_execution::<CurrentAleo, _>( - &locator_s, + locator, VarunaVersion::V2, &mut rand::make_rng::<StdRng>(), ) From be568bd0cade4abf3a06285d7c867e322c50986e Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 14:22:08 -0500 Subject: [PATCH 26/72] ci: parallel GitHub Actions test workflow; drop unused openssl dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sdk.yml: three parallel lanes — lint (fmt/clippy/pyright + testnet cfg check), fast pytest matrix on linux/macos/windows with pytest-xdist, and a proving job with a cached ~/.aleo parameter store - remove unused openssl 0.10 dependency and the openssl/vendored flags from the wheels workflow (never referenced in src/) - fix pyright reportUnusedImport on the aleo.mainnet re-export Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .github/workflows/sdk-wheels.yml | 6 +- .github/workflows/sdk.yml | 96 ++++++++++++++++++++++++-------- sdk/Cargo.lock | 1 - sdk/Cargo.toml | 1 - sdk/python/aleo/__init__.py | 2 +- 5 files changed, 76 insertions(+), 30 deletions(-) diff --git a/.github/workflows/sdk-wheels.yml b/.github/workflows/sdk-wheels.yml index 8ca9487..9551b93 100644 --- a/.github/workflows/sdk-wheels.yml +++ b/.github/workflows/sdk-wheels.yml @@ -40,7 +40,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml --features openssl/vendored + args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml manylinux: auto before-script-linux: | # If we're running on rhel centos, install needed packages. @@ -75,7 +75,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml --features openssl/vendored + args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v3 @@ -98,7 +98,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml --features openssl/vendored + args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v3 diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 712018a..3f412bb 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -3,9 +3,12 @@ name: SDK on: workflow_call: +env: + CARGO_TERM_COLOR: always + jobs: - check: - name: Check + lint: + name: Lint (fmt, clippy, pyright) runs-on: ubuntu-latest defaults: run: @@ -13,43 +16,88 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: sdk - name: fmt run: cargo fmt --check - - name: clippy - run: cargo clippy -- -D warnings + - name: clippy (mainnet) + run: cargo clippy --no-default-features --features mainnet -- -D warnings + # Guard the testnet cfg seam — it only exists at compile time. + - name: check (testnet) + run: cargo check --no-default-features --features testnet - uses: jordemort/action-pyright@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} workdir: ./sdk fail_on_error: true - build_test: - name: Build & Test - needs: [check] + + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + defaults: + run: + working-directory: ./sdk + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: sdk + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Build wheel + run: | + pip install maturin + maturin build --release --features mainnet --out dist + - name: Install wheel + test deps + run: | + pip install --no-index --find-links dist aleo + pip install pytest pytest-xdist + # Fast, offline tests; -n auto parallelizes across cores. + - name: pytest (fast suite) + run: python -m pytest python/tests -v -n auto -m "not slow" + + test-proving: + name: Test (proving, network) runs-on: ubuntu-latest defaults: run: working-directory: ./sdk + shell: bash steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: actions/setup-python@v4 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: sdk + - uses: actions/setup-python@v5 with: - python-version: '3.10' - - name: Build - uses: PyO3/maturin-action@v1 + python-version: '3.12' + # SRS/proving parameters are fetched on first prove; cache them so the + # job costs ~30s warm instead of minutes of downloads. + - name: Cache Aleo parameters + uses: actions/cache@v4 with: - args: --release --features openssl/vendored - working-directory: ./sdk - - name: Install Wheel - run: pip install target/wheels/*.whl - - name: Build & Test RTD + path: ~/.aleo + key: aleo-parameters-${{ runner.os }}-v1 + - name: Build wheel run: | - pip install -r docs/requirements.txt - sphinx-build docs/source/ _readthedocs/ - sphinx-build -b doctest docs/source/ _readthedocs/doctest - - name: Test + pip install maturin + maturin build --release --features mainnet --out dist + - name: Install wheel + test deps run: | - python -m venv .env - source .env/bin/activate - maturin develop --release --features openssl/vendored - python python/test.py + pip install --no-index --find-links dist aleo + pip install pytest + # The two slow tests share one module-scoped proven execution; xdist + # would prove twice, so they run in a single worker on purpose. + - name: pytest (proving suite) + run: python -m pytest python/tests -v -m slow diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index f2ca0c9..215529e 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -30,7 +30,6 @@ dependencies = [ "anyhow", "indexmap", "once_cell", - "openssl", "pyo3", "rand", "serde", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 84c2abb..029ff09 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -22,7 +22,6 @@ testnet = [] anyhow = "1" indexmap = "2.1.0" once_cell = "1.18.0" -openssl = "0.10" pyo3 = { version = "0.20.0", features = ["extension-module", "abi3-py37", "anyhow"] } rand = { version = "0.10" } serde = "1" diff --git a/sdk/python/aleo/__init__.py b/sdk/python/aleo/__init__.py index 90d7d5e..e12a97d 100644 --- a/sdk/python/aleo/__init__.py +++ b/sdk/python/aleo/__init__.py @@ -1,4 +1,4 @@ from __future__ import annotations -from . import mainnet +from . import mainnet as mainnet from .encryptor import * From 7196d4df93b2a8e3b51e51fd7226c237c5ef4d7d Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 14:24:13 -0500 Subject: [PATCH 27/72] style: cargo fmt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- sdk/src/account/text.rs | 6 +++++- sdk/src/types.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/sdk/src/account/text.rs b/sdk/src/account/text.rs index c1cfc83..3855408 100644 --- a/sdk/src/account/text.rs +++ b/sdk/src/account/text.rs @@ -97,7 +97,11 @@ impl Plaintext { #[staticmethod] fn new_struct(kv: Vec<(Identifier, Plaintext)>) -> Self { let kv: Vec<_> = kv.into_iter().map(|(k, v)| (k.into(), v.into())).collect(); - PlaintextNative::Struct(indexmap::IndexMap::from_iter(kv), std::sync::OnceLock::new()).into() + PlaintextNative::Struct( + indexmap::IndexMap::from_iter(kv), + std::sync::OnceLock::new(), + ) + .into() } /// Returns a new Plaintext::Array from a list of values. diff --git a/sdk/src/types.rs b/sdk/src/types.rs index cd6c2b8..5b39a90 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -14,24 +14,24 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -#[cfg(not(feature = "testnet"))] -use snarkvm::circuit::network::AleoV0; #[cfg(feature = "testnet")] use snarkvm::circuit::network::AleoTestnetV0; #[cfg(not(feature = "testnet"))] +use snarkvm::circuit::network::AleoV0; +#[cfg(not(feature = "testnet"))] use snarkvm::console::network::MainnetV0; #[cfg(feature = "testnet")] use snarkvm::console::network::TestnetV0; use snarkvm::ledger::block::{Fee, Transaction}; use snarkvm::ledger::query::Query; use snarkvm::ledger::store::helpers::memory::BlockMemory; -use snarkvm::synthesizer::{Process, Trace}; use snarkvm::prelude::{ - Address, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, Group, - Identifier, Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, ProvingKey, - Record, Response, Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, I128, - I16, I32, I64, I8, U128, U16, U32, U64, U8, + Address, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, Group, Identifier, + Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, ProvingKey, Record, Response, + Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, I128, I16, I32, I64, I8, U128, + U16, U32, U64, U8, }; +use snarkvm::synthesizer::{Process, Trace}; // Network selection (feature-gated; MainnetV0 by default). #[cfg(not(feature = "testnet"))] From 5acd697111484206247912cc82aae688ea0d3eb5 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 14:29:20 -0500 Subject: [PATCH 28/72] ci: scope pyright to the shipped package, exclude tests/venv/build dirs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- sdk/pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index d445f40..97cc1ca 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -26,5 +26,7 @@ module-name = "aleo._aleolib_mainnet" [tool.pyright] reportMissingModuleSource = false # don't report missing aleo.so for CI purposes -ignore = ['./docs'] -strict = ['./'] +# Tests are dynamically-typed pytest code and import deps (pytest) that the +# bare pyright CI environment doesn't install — type-check the package only. +exclude = ['docs', 'python/tests', '.env', 'target'] +strict = ['python/aleo'] From fd7d648f189d56d0baf4c65dcb333017e075fd28 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 15:58:33 -0500 Subject: [PATCH 29/72] ci: remove CircleCI (superseded by GitHub Actions test workflow) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .circleci/config.yml | 45 -------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index bdd59bb..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,45 +0,0 @@ -version: 2.1 - -jobs: - build-and-test: - docker: - - image: cimg/python:3.12.0 - working_directory: ~/project/sdk - steps: - - checkout - # Setting up a virtual environment for the Python project - - run: python3 -m venv .env - - run: source .env/bin/activate - # Install Rust - - run: - name: Install Rust - command: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo 'source $HOME/.cargo/env' >> $BASH_ENV - source $BASH_ENV - - run: rustc --version # just to confirm it's installed - # Install maturin and dependencies - - run: - name: Activate virtualenv and install dependencies - command: | - . .env/bin/activate - pip install maturin[patchelf] - - run: - name: Build and develop the package - command: | - . .env/bin/activate - maturin develop --features mainnet - # Run the unittests - - run: - name: Run tests - command: | - . .env/bin/activate - pip install pytest - # slow proving tests: run locally with -m slow - python -m pytest python/tests -v -m "not slow" - -workflows: - version: 2 - build-and-test-workflow: - jobs: - - build-and-test From c3e37de34af1eeee12703a4cd6bbfeacfc7211a1 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Wed, 8 Jul 2026 15:59:15 -0500 Subject: [PATCH 30/72] =?UTF-8?q?ci:=20rebuild=20wheels=20pipeline=20?= =?UTF-8?q?=E2=80=94=20modern=20actions,=20native=20runners,=20smoke=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - artifact actions v3 -> v4 (v3 is disabled by GitHub, failing every job) - matrix: linux x86_64 + aarch64 (native arm runner, manylinux_2_28), macos x86_64 (macos-13) + aarch64 (macos-latest), windows x64; sdist - drop 32-bit i686/x86 targets (unsupported by snarkvm 4.x / aws-lc-sys) - manylinux container gets git (snarkvm git dep) + cmake/clang (aws-lc-sys) - every wheel is installed and smoke-tested on its native runner - abi3 wheels, --features mainnet, sccache; tag-triggered PyPI publish kept Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .github/workflows/sdk-wheels.yml | 145 ++++++++++++++----------------- 1 file changed, 66 insertions(+), 79 deletions(-) diff --git a/.github/workflows/sdk-wheels.yml b/.github/workflows/sdk-wheels.yml index 9551b93..8f2c662 100644 --- a/.github/workflows/sdk-wheels.yml +++ b/.github/workflows/sdk-wheels.yml @@ -1,14 +1,11 @@ -# This file is autogenerated by maturin v1.3.1 -# To update, run -# -# maturin generate-ci --platform linux --platform macos --platform windows --manifest-path ./sdk/Cargo.toml --output .github/workflows/sdk-wheels.yml github -# name: SDK Wheels on: push: paths: - 'sdk/**' + - '.github/workflows/sdk-wheels.yml' + - '.github/workflows/sdk.yml' branches: - main - master @@ -17,93 +14,81 @@ on: pull_request: paths: - 'sdk/**' + - '.github/workflows/sdk-wheels.yml' + - '.github/workflows/sdk.yml' workflow_dispatch: permissions: contents: read +# The crate is abi3-py37, so each platform needs exactly one wheel that works +# on every CPython >= 3.7. 32-bit targets (i686/x86) are deliberately dropped: +# snarkvm 4.x and its aws-lc-sys dependency do not support them. jobs: tests: uses: ./.github/workflows/sdk.yml - linux: - needs: [tests] - runs-on: ubuntu-latest - strategy: - matrix: - target: [x86_64, x86] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml - manylinux: auto - before-script-linux: | - # If we're running on rhel centos, install needed packages. - if command -v yum &> /dev/null; then - yum update -y && yum install -y perl-core libatomic - # If we're running on i686 we need to symlink libatomic - # in order to build openssl with -latomic flag. - if [[ ! -d "/usr/lib64" ]]; then - ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so - fi - fi - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - windows: + build: + name: build (${{ matrix.platform.name }}) needs: [tests] - runs-on: windows-latest + runs-on: ${{ matrix.platform.runner }} strategy: + fail-fast: false matrix: - target: [x64, x86] + platform: + - name: linux-x86_64 + runner: ubuntu-latest + target: x86_64 + - name: linux-aarch64 + # native arm runner — avoids cross-compiling aws-lc-sys C code + runner: ubuntu-24.04-arm + target: aarch64 + - name: macos-x86_64 + runner: macos-13 + target: x86_64 + - name: macos-aarch64 + runner: macos-latest + target: aarch64 + - name: windows-x64 + runner: windows-latest + target: x64 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: - python-version: '3.10' - architecture: ${{ matrix.target }} - - name: Build wheels + python-version: '3.12' + - name: Build wheel uses: PyO3/maturin-action@v1 with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml + target: ${{ matrix.platform.target }} + args: --release --out dist --features mainnet --manifest-path ./sdk/Cargo.toml sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - macos: - needs: [tests] - runs-on: macos-latest - strategy: - matrix: - target: [x86_64, aarch64] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ./sdk/Cargo.toml - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 + manylinux: ${{ startsWith(matrix.platform.name, 'linux') && '2_28' || 'auto' }} + # The manylinux container needs git (snarkvm is a git dependency) + # and cmake + a C toolchain (aws-lc-sys builds C sources). + before-script-linux: | + if command -v dnf &> /dev/null; then + dnf install -y git cmake clang + elif command -v yum &> /dev/null; then + yum install -y git cmake3 clang && ln -sf "$(command -v cmake3)" /usr/local/bin/cmake + fi + # Every runner here matches its wheel's platform, so install and smoke + # test the actual artifact before it can ship. + - name: Smoke test wheel + shell: bash + run: | + pip install --no-index --find-links dist aleo + python -c " + from aleo.mainnet import PrivateKey + pk = PrivateKey.random() + assert str(pk).startswith('APrivateKey1') + assert str(pk.address).startswith('aleo1') + print('wheel ok:', pk.address) + " + - name: Upload wheel + uses: actions/upload-artifact@v4 with: - name: wheels + name: wheels-${{ matrix.platform.name }} path: dist sdist: @@ -117,24 +102,26 @@ jobs: command: sdist args: --out dist --manifest-path ./sdk/Cargo.toml - name: Upload sdist - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels + name: wheels-sdist path: dist release: name: Release runs-on: ubuntu-latest if: "startsWith(github.ref, 'refs/tags/')" - needs: [linux, windows, macos, sdist] + needs: [build, sdist] steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: wheels + pattern: wheels-* + merge-multiple: true + path: dist - name: Publish to PyPI uses: PyO3/maturin-action@v1 env: MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} with: command: upload - args: --non-interactive --skip-existing * + args: --non-interactive --skip-existing dist/* From 809587a3372e700ef09ce946e33df8ee4cfb28c8 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 03:58:09 -0500 Subject: [PATCH 31/72] ci: run maturin from sdk/ so cargo honors sdk/.cargo/config.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo resolves .cargo/config.toml from the working directory, not the manifest path — the macOS wheel link needs those dynamic_lookup flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .github/workflows/sdk-wheels.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sdk-wheels.yml b/.github/workflows/sdk-wheels.yml index 8f2c662..2487d03 100644 --- a/.github/workflows/sdk-wheels.yml +++ b/.github/workflows/sdk-wheels.yml @@ -60,8 +60,12 @@ jobs: - name: Build wheel uses: PyO3/maturin-action@v1 with: + # Run from sdk/ so cargo picks up sdk/.cargo/config.toml (macOS + # needs its dynamic_lookup link flags; cargo resolves config from + # the working directory, not the manifest path). + working-directory: ./sdk target: ${{ matrix.platform.target }} - args: --release --out dist --features mainnet --manifest-path ./sdk/Cargo.toml + args: --release --out dist --features mainnet sccache: 'true' manylinux: ${{ startsWith(matrix.platform.name, 'linux') && '2_28' || 'auto' }} # The manylinux container needs git (snarkvm is a git dependency) @@ -77,7 +81,7 @@ jobs: - name: Smoke test wheel shell: bash run: | - pip install --no-index --find-links dist aleo + pip install --no-index --find-links sdk/dist aleo python -c " from aleo.mainnet import PrivateKey pk = PrivateKey.random() @@ -89,7 +93,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.platform.name }} - path: dist + path: sdk/dist sdist: needs: [tests] @@ -99,13 +103,14 @@ jobs: - name: Build sdist uses: PyO3/maturin-action@v1 with: + working-directory: ./sdk command: sdist - args: --out dist --manifest-path ./sdk/Cargo.toml + args: --out dist - name: Upload sdist uses: actions/upload-artifact@v4 with: name: wheels-sdist - path: dist + path: sdk/dist release: name: Release From 293977734acd64a3e6fc523fce42d98da6250340 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 06:00:39 -0400 Subject: [PATCH 32/72] test: expand coverage with TS-SDK vectors (accounts, records, poseidon, plaintext, types) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mine ProvableHQ/sdk@543b41e for test vectors and patterns; add 76 new tests across 4 files covering KATs (seed→key, 4th derivation pair, Poseidon2 hash, signature literal, battleship record), Account class, Plaintext construction/encryption, and primitive types (integers, ProgramID, Identifier, Locator, Program, bytes round-trips). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_account_class.py | 114 ++++++++++++ sdk/python/tests/test_plaintext.py | 217 ++++++++++++++++++++++ sdk/python/tests/test_types.py | 241 +++++++++++++++++++++++++ sdk/python/tests/test_vectors_extra.py | 179 ++++++++++++++++++ 4 files changed, 751 insertions(+) create mode 100644 sdk/python/tests/test_account_class.py create mode 100644 sdk/python/tests/test_plaintext.py create mode 100644 sdk/python/tests/test_types.py create mode 100644 sdk/python/tests/test_vectors_extra.py diff --git a/sdk/python/tests/test_account_class.py b/sdk/python/tests/test_account_class.py new file mode 100644 index 0000000..c2d5d75 --- /dev/null +++ b/sdk/python/tests/test_account_class.py @@ -0,0 +1,114 @@ +""" +Tests for the Account class surface. +No TS SDK vector file specific to Account class; vectors reuse existing +records.json KATs (view_key/ciphertext). +""" + +import pytest +from aleo.mainnet import Account, PrivateKey, RecordCiphertext, ViewKey +from conftest import load_vectors + + +_MESSAGE = b"hello world" + +# Known private key (from accounts.json triple 0 / account-data.ts beaconKey) +_KNOWN_PRIVKEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" +_KNOWN_VIEWKEY = "AViewKey1mSnpFFC8Mj4fXbK5YiWgZ3mjiV8CxA79bYNa8ymUpTrw" +_KNOWN_ADDRESS = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" + + +class TestAccountRandom: + def test_random_returns_account(self): + acc = Account.random() + assert acc is not None + + def test_random_properties_accessible(self): + acc = Account.random() + pk_str = str(acc.private_key) + vk_str = str(acc.view_key) + addr_str = str(acc.address) + assert pk_str.startswith("APrivateKey1") + assert vk_str.startswith("AViewKey1") + assert addr_str.startswith("aleo1") + + def test_random_unique(self): + """Two random accounts should (with overwhelming probability) differ.""" + a1 = Account.random() + a2 = Account.random() + assert str(a1.private_key) != str(a2.private_key) + + +class TestAccountFromPrivateKey: + def test_from_private_key_round_trips(self): + pk = PrivateKey.from_string(_KNOWN_PRIVKEY) + acc = Account.from_private_key(pk) + assert str(acc.private_key) == _KNOWN_PRIVKEY + assert str(acc.view_key) == _KNOWN_VIEWKEY + assert str(acc.address) == _KNOWN_ADDRESS + + def test_from_random_private_key(self): + pk = PrivateKey.random() + acc = Account.from_private_key(pk) + assert str(acc.address) == str(pk.address) + assert str(acc.view_key) == str(pk.view_key) + + +class TestAccountSignVerify: + def test_sign_verify_own_message(self): + acc = Account.random() + sig = acc.sign(_MESSAGE) + assert acc.verify(sig, _MESSAGE) is True + + def test_verify_tampered_message_fails(self): + acc = Account.random() + sig = acc.sign(_MESSAGE) + assert acc.verify(sig, b"tampered") is False + + def test_cross_account_verify_fails(self): + acc1 = Account.random() + acc2 = Account.random() + sig = acc1.sign(_MESSAGE) + assert acc2.verify(sig, _MESSAGE) is False + + +class TestAccountDecryptIsOwner: + """Use existing records.json KAT ciphertext to validate decrypt/is_owner.""" + + def _kat(self): + return load_vectors("records.json")["decrypt_kat"] + + def test_is_owner_true(self): + v = self._kat() + pk = PrivateKey.from_string(v["private_key"]) + acc = Account.from_private_key(pk) + ct = RecordCiphertext.from_string(v["ciphertext"]) + assert acc.is_owner(ct) is True + + def test_is_owner_false_for_foreign(self): + """Beacon account must not own the 'foreign' ciphertext (account-data.ts).""" + acc = Account.from_private_key( + PrivateKey.from_string(_KNOWN_PRIVKEY) + ) + foreign_ct = RecordCiphertext.from_string( + "record1qyqsq553yxz8ylwqyqfmcfmwz03x6xsxf2h2kypcwhykzgm50ut4sus" + "yqyxx66trwfhkxun9v35hguerqqpqzqyjt8kxnp28v83t460knvp0dq86a3r3dy" + "ve945u0xqeksq323paqtegslprdc5zypksrja7rmctx90jnpeq5sqkwlfct7ygy9" + "90a5pqs7y5pt0" + ) + assert acc.is_owner(foreign_ct) is False + + def test_decrypt_returns_correct_owner(self): + v = self._kat() + pk = PrivateKey.from_string(v["private_key"]) + acc = Account.from_private_key(pk) + ct = RecordCiphertext.from_string(v["ciphertext"]) + pt = acc.decrypt(ct) + assert pt.owner.split(".")[0] == v["owner"] + + def test_decrypt_contains_microcredits(self): + v = self._kat() + pk = PrivateKey.from_string(v["private_key"]) + acc = Account.from_private_key(pk) + ct = RecordCiphertext.from_string(v["ciphertext"]) + pt = acc.decrypt(ct) + assert f"{v['microcredits']}u64" in str(pt) diff --git a/sdk/python/tests/test_plaintext.py b/sdk/python/tests/test_plaintext.py new file mode 100644 index 0000000..d225400 --- /dev/null +++ b/sdk/python/tests/test_plaintext.py @@ -0,0 +1,217 @@ +""" +Tests for Plaintext construction, classification, encrypt/decrypt, and +related Literal, Ciphertext, and Value types. +""" + +import pytest +from aleo.mainnet import ( + Address, + Boolean, + Ciphertext, + Field, + Group, + I128, + Identifier, + Literal, + Plaintext, + Scalar, + Signature, + U8, + U64, + Value, +) + + +# --------------------------------------------------------------------------- +# Literal constructors and type_name +# --------------------------------------------------------------------------- + +class TestLiteral: + def test_from_field(self): + lit = Literal.from_field(Field.from_string("42field")) + assert lit.type_name() == "field" + + def test_from_address(self): + addr = Address.from_string( + "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" + ) + lit = Literal.from_address(addr) + assert lit.type_name() == "address" + + def test_from_boolean(self): + lit = Literal.from_boolean(Boolean(True)) + assert lit.type_name() == "boolean" + + def test_from_u8(self): + lit = Literal.from_u8(U8(255)) + assert lit.type_name() == "u8" + + def test_from_u64(self): + lit = Literal.from_u64(U64(1_000_000)) + assert lit.type_name() == "u64" + + def test_from_i128(self): + lit = Literal.from_i128(I128(-1)) + assert lit.type_name() == "i128" + + def test_from_scalar(self): + lit = Literal.from_scalar(Scalar.zero()) + assert lit.type_name() == "scalar" + + def test_from_signature(self): + sig = Signature.from_string( + "sign1lcpxtgqkp238x45fk79lkx5xz7sx37f56wl0hyemhv78dgzxyspykg6u26l" + "x2a02tvat6zaflx530qtnme34gh702wclwr20rdxrsqcl7shvwsyhygt2yvkgzeq7" + "zz2rdat4rrsr0cd9kwm6jddjcs9lps8s80v35rwvtkgg2gxprf4dge0tcet3pe7nf" + "xupkvfuvh3sw2gpyv0km46" + ) + lit = Literal.from_signature(sig) + assert lit.type_name() == "signature" + + def test_parse_u64(self): + lit = Literal.parse("99u64") + assert lit.type_name() == "u64" + + def test_parse_address(self): + lit = Literal.parse( + "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" + ) + assert lit.type_name() == "address" + + +# --------------------------------------------------------------------------- +# Plaintext: new_literal / is_literal / as_literal +# --------------------------------------------------------------------------- + +class TestPlaintextLiteral: + def test_new_literal_classification(self): + p = Plaintext.new_literal(Literal.from_field(Field.from_string("1field"))) + assert p.is_literal() is True + assert p.is_struct() is False + assert p.is_array() is False + + def test_as_literal_round_trip(self): + lit = Literal.from_field(Field.from_string("77field")) + p = Plaintext.new_literal(lit) + lit2 = p.as_literal() + assert lit2.type_name() == "field" + + def test_from_string_literal(self): + p = Plaintext.from_string("42u64") + assert p.is_literal() is True + + +# --------------------------------------------------------------------------- +# Plaintext: new_struct / is_struct / as_struct +# --------------------------------------------------------------------------- + +class TestPlaintextStruct: + def _make_struct(self): + k1 = Identifier.from_string("x") + v1 = Plaintext.new_literal(Literal.from_field(Field.from_string("1field"))) + k2 = Identifier.from_string("y") + v2 = Plaintext.new_literal(Literal.from_u64(U64(99))) + return Plaintext.new_struct([(k1, v1), (k2, v2)]) + + def test_new_struct_classification(self): + ps = self._make_struct() + assert ps.is_struct() is True + assert ps.is_literal() is False + assert ps.is_array() is False + + def test_as_struct_has_correct_members(self): + ps = self._make_struct() + members = ps.as_struct() + assert len(members) == 2 + + def test_struct_singleton(self): + k = Identifier.from_string("a") + v = Plaintext.new_literal(Literal.from_boolean(Boolean(False))) + ps = Plaintext.new_struct([(k, v)]) + assert ps.is_struct() is True + assert len(ps.as_struct()) == 1 + + +# --------------------------------------------------------------------------- +# Plaintext: new_array / is_array / as_array +# --------------------------------------------------------------------------- + +class TestPlaintextArray: + def test_new_array_classification(self): + elem = Plaintext.new_literal(Literal.from_u64(U64(0))) + pa = Plaintext.new_array([elem, elem, elem]) + assert pa.is_array() is True + assert pa.is_literal() is False + assert pa.is_struct() is False + + def test_as_array_length(self): + elems = [ + Plaintext.new_literal(Literal.from_u8(U8(i))) for i in range(4) + ] + pa = Plaintext.new_array(elems) + assert len(pa.as_array()) == 4 + + def test_empty_array(self): + pa = Plaintext.new_array([]) + assert pa.is_array() is True + assert len(pa.as_array()) == 0 + + +# --------------------------------------------------------------------------- +# Plaintext: encrypt_symmetric / decrypt_symmetric round-trip +# --------------------------------------------------------------------------- + +class TestSymmetricEncryption: + def _key(self): + return Field.from_string("12345678field") + + def test_literal_round_trip(self): + key = self._key() + p = Plaintext.new_literal(Literal.from_field(Field.from_string("99field"))) + ct = p.encrypt_symmetric(key) + assert isinstance(ct, Ciphertext) + p2 = ct.decrypt_symmetric(key) + assert str(p2) == str(p) + + def test_struct_round_trip(self): + key = self._key() + k = Identifier.from_string("v") + v = Plaintext.new_literal(Literal.from_u64(U64(42_000))) + ps = Plaintext.new_struct([(k, v)]) + ct = ps.encrypt_symmetric(key) + ps2 = ct.decrypt_symmetric(key) + assert str(ps2) == str(ps) + + def test_wrong_key_produces_different_plaintext_or_errors(self): + """Decrypting with the wrong key must not silently return the original plaintext. + snarkvm may raise a RuntimeError (malformed struct) or return a different value; + either outcome is correct – the ciphertext must not decrypt to the original.""" + key1 = Field.from_string("1field") + key2 = Field.from_string("2field") + p = Plaintext.new_literal(Literal.from_field(Field.from_string("7field"))) + ct = p.encrypt_symmetric(key1) + try: + p_wrong = ct.decrypt_symmetric(key2) + assert str(p_wrong) != str(p) + except RuntimeError: + pass # snarkvm raised an error – wrong key rejected + + +# --------------------------------------------------------------------------- +# Value +# --------------------------------------------------------------------------- + +class TestValue: + def test_parse_u64(self): + v = Value.parse("42u64") + assert "42u64" in str(v) + + def test_parse_address(self): + addr_str = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" + v = Value.parse(addr_str) + assert addr_str in str(v) + + def test_from_literal(self): + lit = Literal.from_field(Field.from_string("7field")) + v = Value.from_literal(lit) + assert "7field" in str(v) diff --git a/sdk/python/tests/test_types.py b/sdk/python/tests/test_types.py new file mode 100644 index 0000000..b693a1b --- /dev/null +++ b/sdk/python/tests/test_types.py @@ -0,0 +1,241 @@ +""" +Tests for SDK primitive and structural types: + - Integer types (U8, U16, U32, U64, U128, I8, I16, I32, I64, I128) + - ProgramID, Identifier, Locator + - Program.from_source + - Bytes round-trips (Authorization) +""" + +import pytest +from aleo.mainnet import ( + Authorization, + I8, + I16, + I32, + I64, + I128, + Identifier, + Locator, + Network, + PrivateKey, + Process, + Program, + ProgramID, + U8, + U16, + U32, + U64, + U128, + Value, +) + + +# --------------------------------------------------------------------------- +# Integer types +# --------------------------------------------------------------------------- + +class TestUnsignedIntegers: + def test_u8_construction_and_int(self): + assert int(U8(0)) == 0 + assert int(U8(255)) == 255 + + def test_u8_zero(self): + assert int(U8.zero()) == 0 + + def test_u16_construction(self): + assert int(U16(65535)) == 65535 + assert int(U16.zero()) == 0 + + def test_u32_construction(self): + assert int(U32(4_294_967_295)) == 4_294_967_295 + assert int(U32.zero()) == 0 + + def test_u64_construction(self): + assert int(U64(1_000_000)) == 1_000_000 + assert int(U64.zero()) == 0 + + def test_u128_construction(self): + large = (1 << 64) + 1 + assert int(U128(large)) == large + assert int(U128.zero()) == 0 + + +class TestSignedIntegers: + def test_i8_construction_and_int(self): + assert int(I8(-128)) == -128 + assert int(I8(127)) == 127 + + def test_i8_zero(self): + assert int(I8.zero()) == 0 + + def test_i16_construction(self): + assert int(I16(-32768)) == -32768 + assert int(I16.zero()) == 0 + + def test_i32_construction(self): + assert int(I32(-1)) == -1 + assert int(I32.zero()) == 0 + + def test_i64_construction(self): + assert int(I64(-(1 << 62))) == -(1 << 62) + assert int(I64.zero()) == 0 + + def test_i128_construction(self): + assert int(I128(-999_999_999)) == -999_999_999 + assert int(I128.zero()) == 0 + + +# --------------------------------------------------------------------------- +# ProgramID +# --------------------------------------------------------------------------- + +class TestProgramID: + def test_credits_parse(self): + pid = ProgramID.from_string("credits.aleo") + assert str(pid.name) == "credits" + assert str(pid.network) == "aleo" + + def test_str_round_trip(self): + pid = ProgramID.from_string("hello.aleo") + assert str(pid) == "hello.aleo" + + def test_is_aleo(self): + pid = ProgramID.from_string("credits.aleo") + assert pid.is_aleo() is True + + def test_hashable_as_dict_key(self): + pid1 = ProgramID.from_string("credits.aleo") + pid2 = ProgramID.from_string("credits.aleo") + d = {pid1: "ok"} + assert pid2 in d + + def test_equality(self): + pid1 = ProgramID.from_string("credits.aleo") + pid2 = ProgramID.from_string("credits.aleo") + assert pid1 == pid2 + + def test_inequality(self): + pid1 = ProgramID.from_string("credits.aleo") + pid2 = ProgramID.from_string("token.aleo") + assert pid1 != pid2 + + +# --------------------------------------------------------------------------- +# Identifier +# --------------------------------------------------------------------------- + +class TestIdentifier: + def test_from_string_round_trip(self): + ident = Identifier.from_string("transfer_public") + assert str(ident) == "transfer_public" + + def test_equality(self): + a = Identifier.from_string("noop") + b = Identifier.from_string("noop") + assert a == b + + def test_inequality(self): + a = Identifier.from_string("foo") + b = Identifier.from_string("bar") + assert a != b + + def test_hashable(self): + a = Identifier.from_string("mint") + b = Identifier.from_string("mint") + s = {a} + assert b in s + + +# --------------------------------------------------------------------------- +# Locator +# --------------------------------------------------------------------------- + +class TestLocator: + def test_from_string_properties(self): + loc = Locator.from_string("credits.aleo/transfer_public") + assert str(loc.program_id) == "credits.aleo" + assert str(loc.resource) == "transfer_public" + assert str(loc.name) == "credits" + assert str(loc.network) == "aleo" + + def test_constructor(self): + pid = ProgramID.from_string("credits.aleo") + res = Identifier.from_string("fee_public") + loc = Locator(pid, res) + assert str(loc.resource) == "fee_public" + + def test_str_round_trip(self): + loc = Locator.from_string("credits.aleo/transfer_public") + assert str(loc) == "credits.aleo/transfer_public" + + +# --------------------------------------------------------------------------- +# Program.from_source +# --------------------------------------------------------------------------- + +_NOOP_SRC = "program test.aleo;\nfunction noop:\n" +_HELLO_SRC = ( + "program hellothere.aleo;\n" + "\n" + "function hello:\n" + " input r0 as u32.public;\n" + " input r1 as u32.private;\n" + " add r0 r1 into r2;\n" + " output r2 as u32.private;\n" +) + + +class TestProgram: + def test_noop_program_parses(self): + prog = Program.from_source(_NOOP_SRC) + assert str(prog.id) == "test.aleo" + + def test_noop_function_list(self): + prog = Program.from_source(_NOOP_SRC) + assert Identifier.from_string("noop") in prog.functions + + def test_noop_source_round_trip(self): + prog = Program.from_source(_NOOP_SRC) + assert "program test.aleo" in prog.source + + def test_hello_program_from_ts_sdk(self): + """hellothere.aleo – exact program from TS SDK account-data.ts helloProgram.""" + prog = Program.from_source(_HELLO_SRC) + assert str(prog.id) == "hellothere.aleo" + assert Identifier.from_string("hello") in prog.functions + + def test_credits_is_builtin(self): + prog = Program.credits() + assert str(prog.id) == "credits.aleo" + assert len(prog.functions) > 0 + + def test_network_name_and_id(self): + # Network.name() returns the full display name (e.g. "Aleo Mainnet (v0)"); + # Network.id() is 0 for MainnetV0. + assert "mainnet" in Network.name().lower() or "aleo" in Network.name().lower() + assert Network.id() == 0 + + +# --------------------------------------------------------------------------- +# Bytes round-trips +# --------------------------------------------------------------------------- + +class TestBytesRoundTrips: + """bytes() → from_bytes() for Authorization.""" + + def _make_auth(self): + process = Process.load() + pk = PrivateKey.random() + return process.authorize( + pk, + ProgramID.from_string("credits.aleo"), + Identifier.from_string("transfer_public"), + [Value.parse(str(pk.address)), Value.parse("10u64")], + ) + + def test_authorization_bytes_round_trip(self): + auth = self._make_auth() + raw = auth.bytes() + assert isinstance(raw, list) + auth2 = Authorization.from_bytes(bytes(raw)) + assert auth.to_json() == auth2.to_json() diff --git a/sdk/python/tests/test_vectors_extra.py b/sdk/python/tests/test_vectors_extra.py new file mode 100644 index 0000000..5dcea44 --- /dev/null +++ b/sdk/python/tests/test_vectors_extra.py @@ -0,0 +1,179 @@ +""" +KAT tests mined from the ProvableHQ TypeScript SDK. +Reference: github.com/ProvableHQ/sdk@543b41e0a7e3d6a8a49ae8d0870809dc92b88684 + +Sources: + sdk/tests/data/account-data.ts – seed, keys, ciphertexts + sdk/tests/data/algebra.ts – FieldGenerator, expectedPoseidon2Hash + wasm/src/account/view_key.rs:182-183 – 4th derivation pair + wasm/src/algorithms/bhp/mod.rs:162 – signature literal + wasm/src/record/record_plaintext.rs:382-425,504-514 – battleship record +""" + +import pytest +from aleo.mainnet import ( + Field, + Network, + PrivateKey, + RecordCiphertext, + RecordPlaintext, + Signature, + ViewKey, +) + +# --------------------------------------------------------------------------- +# Seed → private-key KAT +# source: sdk/tests/data/account-data.ts (TS Uint8Array seed → beacon key) +# The TS wasm binding uses Field::from_bytes_le_mod_order(seed_bytes); our +# Python surface exposes PrivateKey.from_seed(Field). We reconstruct the +# Field by interpreting the 32 seed bytes as a little-endian integer. +# --------------------------------------------------------------------------- + +_SEED_BYTES = [ + 94, 91, 52, 251, 240, 230, 226, 35, 117, 253, 224, 210, 175, 13, 205, 120, + 155, 214, 7, 169, 66, 62, 206, 50, 188, 40, 29, 122, 40, 250, 54, 18, +] +_SEED_PRIVKEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" +_SEED_ADDRESS = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" + + +def test_seed_to_private_key_kat(): + """32-byte LE seed → known private key (account-data.ts beacon key).""" + n = int.from_bytes(bytes(_SEED_BYTES), "little") + seed_field = Field.from_string(f"{n}field") + pk = PrivateKey.from_seed(seed_field) + assert str(pk) == _SEED_PRIVKEY + assert str(pk.address) == _SEED_ADDRESS + + +# --------------------------------------------------------------------------- +# 4th derivation pair +# source: wasm/src/account/view_key.rs:182-183 +# --------------------------------------------------------------------------- + +_FOURTH_PRIVKEY = "APrivateKey1zkp4RyQ8Utj7aRcJgPQGEok8RMzWwUZzBhhgX6rhmBT8dcP" +_FOURTH_VIEWKEY = "AViewKey1i3fn5SECcVBtQMCVtTPSvdApoMYmg3ToJfNDfgHJAuoD" + + +def test_fourth_derivation_pair(): + """Private-key → view-key derivation (view_key.rs:182-183).""" + pk = PrivateKey.from_string(_FOURTH_PRIVKEY) + assert str(pk.view_key) == _FOURTH_VIEWKEY + + +# --------------------------------------------------------------------------- +# Foreign record decrypt KAT +# source: sdk/tests/data/account-data.ts foreignCiphertextString / foreignViewKeyString +# The beacon key must NOT own this ciphertext. Actual ownership/decrypt is +# verified: foreignViewKey.is_owner → True; beaconViewKey.is_owner → False. +# Note: RecordCiphertext.decrypt() on this vector returns 'Insufficient bits' +# at the snarkvm level (short record format); ownership/non-ownership check is +# the meaningful assertion here, matching the TS SDK test (account.test.ts:163). +# --------------------------------------------------------------------------- + +_FOREIGN_CT = ( + "record1qyqsq553yxz8ylwqyqfmcfmwz03x6xsxf2h2kypcwhykzgm50ut4sus" + "yqyxx66trwfhkxun9v35hguerqqpqzqyjt8kxnp28v83t460knvp0dq86a3r3dy" + "ve945u0xqeksq323paqtegslprdc5zypksrja7rmctx90jnpeq5sqkwlfct7ygy9" + "90a5pqs7y5pt0" +) +_FOREIGN_VK = "AViewKey1ghtvuJQQzQ31xSiVh6X1PK8biEVhQBygRGV4KdYmq4JT" +_BEACON_VK = "AViewKey1mSnpFFC8Mj4fXbK5YiWgZ3mjiV8CxA79bYNa8ymUpTrw" + + +def test_foreign_record_ownership_kat(): + """Foreign ciphertext: beaconViewKey must not be its owner.""" + ct = RecordCiphertext.from_string( + "record1qyqsq553yxz8ylwqyqfmcfmwz03x6xsxf2h2kypcwhykzgm50ut4sus" + "yqyxx66trwfhkxun9v35hguerqqpqzqyjt8kxnp28v83t460knvp0dq86a3r3dy" + "ve945u0xqeksq323paqtegslprdc5zypksrja7rmctx90jnpeq5sqkwlfct7ygy9" + "90a5pqs7y5pt0" + ) + beacon_vk = ViewKey.from_string(_BEACON_VK) + assert ct.is_owner(beacon_vk) is False + + +# --------------------------------------------------------------------------- +# Poseidon2 hash KAT +# source: sdk/tests/data/algebra.ts + sdk/tests/algorithm.test.ts +# FieldGenerator Fg = 6901184695964460143517399399785179769303979738604374595034454667750561389951field +# Input [Fg, Fg², Fg³, Fg⁴]; hash_psd2 → expectedPoseidon2Hash +# --------------------------------------------------------------------------- + +_FG = "6901184695964460143517399399785179769303979738604374595034454667750561389951field" +_POSEIDON2_EXPECTED = "5077032915756006405056357976663159304886914340125619713231037384461532417432field" + + +def test_poseidon2_hash_kat(): + """Poseidon2 hash of [Fg, Fg^2, Fg^3, Fg^4] equals the algebra.ts vector.""" + Fg = Field.from_string(_FG) + F2 = Fg * Fg + F3 = F2 * Fg + F4 = F3 * Fg + result = Network.hash_psd2([Fg, F2, F3, F4]) + assert str(result) == _POSEIDON2_EXPECTED + + +# --------------------------------------------------------------------------- +# Signature literal round-trip +# source: wasm/src/algorithms/bhp/mod.rs:162 +# --------------------------------------------------------------------------- + +_SIG_LITERAL = ( + "sign1lcpxtgqkp238x45fk79lkx5xz7sx37f56wl0hyemhv78dgzxyspykg6u26l" + "x2a02tvat6zaflx530qtnme34gh702wclwr20rdxrsqcl7shvwsyhygt2yvkgzeq7" + "zz2rdat4rrsr0cd9kwm6jddjcs9lps8s80v35rwvtkgg2gxprf4dge0tcet3pe7nf" + "xupkvfuvh3sw2gpyv0km46" +) + + +def test_signature_literal_roundtrip(): + """Signature.from_string → str() round-trip and property accessibility.""" + sig = Signature.from_string(_SIG_LITERAL) + assert str(sig) == _SIG_LITERAL + # Properties must be accessible and non-empty + challenge_str = str(sig.challenge) + response_str = str(sig.response) + ck_addr_str = str(sig.compute_key.address) + assert challenge_str.endswith("scalar") + assert response_str.endswith("scalar") + assert ck_addr_str.startswith("aleo1") + + +# --------------------------------------------------------------------------- +# Battleship record – member access +# source: wasm/src/record/record_plaintext.rs:382-425, 504-514 +# --------------------------------------------------------------------------- + +_BATTLESHIP_RECORD = """{ + owner: aleo1kh5t7m30djl0ecdn4f5vuzp7dx0tcwh7ncquqjkm4matj2p2zqpqm6at48.private, + metadata: { + player1: aleo1kh5t7m30djl0ecdn4f5vuzp7dx0tcwh7ncquqjkm4matj2p2zqpqm6at48.private, + player2: aleo1dreuxnmg9cny8ee9v2u0wr4v4affnwm09u2pytfwz0f2en2shgqsdsfjn6.private, + nonce: 660310649780728486489183263981322848354071976582883879926426319832534836534field.private + }, + id: 1953278585719525811355617404139099418855053112960441725284031425961000152405field.private, + positions: 50794271u64.private, + attempts: 0u64.private, + hits: 0u64.private, + _nonce: 5668100912391182624073500093436664635767788874314097667746354181784048204413group.public, + _version: 0u8.public +}""" + +_BATTLESHIP_OWNER = "aleo1kh5t7m30djl0ecdn4f5vuzp7dx0tcwh7ncquqjkm4matj2p2zqpqm6at48" + + +def test_battleship_record_owner(): + """RecordPlaintext.owner for the battleship record (record_plaintext.rs:511-514).""" + rec = RecordPlaintext.from_string(_BATTLESHIP_RECORD) + # .owner returns "address.private"; strip visibility qualifier + assert rec.owner.split(".")[0] == _BATTLESHIP_OWNER + + +def test_battleship_record_nonce_and_version(): + """Battleship record nonce and version fields are correct.""" + rec = RecordPlaintext.from_string(_BATTLESHIP_RECORD) + assert str(rec.nonce) == ( + "5668100912391182624073500093436664635767788874314097667746354181784048204413group" + ) + assert rec.version == 0 From f15f72e5cf07ac32e75e91042e4cb819d6af6225 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 06:06:05 -0400 Subject: [PATCH 33/72] test: strengthen foreign-record negatives, drop vacuous test (review fixes) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- sdk/python/tests/test_account_class.py | 4 ---- sdk/python/tests/test_vectors_extra.py | 26 +++++++++++++------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/sdk/python/tests/test_account_class.py b/sdk/python/tests/test_account_class.py index c2d5d75..cb5ec0e 100644 --- a/sdk/python/tests/test_account_class.py +++ b/sdk/python/tests/test_account_class.py @@ -18,10 +18,6 @@ class TestAccountRandom: - def test_random_returns_account(self): - acc = Account.random() - assert acc is not None - def test_random_properties_accessible(self): acc = Account.random() pk_str = str(acc.private_key) diff --git a/sdk/python/tests/test_vectors_extra.py b/sdk/python/tests/test_vectors_extra.py index 5dcea44..d4d3c56 100644 --- a/sdk/python/tests/test_vectors_extra.py +++ b/sdk/python/tests/test_vectors_extra.py @@ -62,13 +62,12 @@ def test_fourth_derivation_pair(): # --------------------------------------------------------------------------- -# Foreign record decrypt KAT +# Foreign record non-ownership KAT # source: sdk/tests/data/account-data.ts foreignCiphertextString / foreignViewKeyString -# The beacon key must NOT own this ciphertext. Actual ownership/decrypt is -# verified: foreignViewKey.is_owner → True; beaconViewKey.is_owner → False. -# Note: RecordCiphertext.decrypt() on this vector returns 'Insufficient bits' -# at the snarkvm level (short record format); ownership/non-ownership check is -# the meaningful assertion here, matching the TS SDK test (account.test.ts:163). +# The TS SDK only ever asserts NON-ownership for this ciphertext +# (account.test.ts:163); it never successfully decrypts it with any key. +# Verified against our binding: neither the beacon view key nor the "foreign" +# view key owns it, and decrypt raises (snarkvm 'Insufficient bits'). # --------------------------------------------------------------------------- _FOREIGN_CT = ( @@ -82,15 +81,16 @@ def test_fourth_derivation_pair(): def test_foreign_record_ownership_kat(): - """Foreign ciphertext: beaconViewKey must not be its owner.""" - ct = RecordCiphertext.from_string( - "record1qyqsq553yxz8ylwqyqfmcfmwz03x6xsxf2h2kypcwhykzgm50ut4sus" - "yqyxx66trwfhkxun9v35hguerqqpqzqyjt8kxnp28v83t460knvp0dq86a3r3dy" - "ve945u0xqeksq323paqtegslprdc5zypksrja7rmctx90jnpeq5sqkwlfct7ygy9" - "90a5pqs7y5pt0" - ) + """Foreign ciphertext: neither key owns it; decrypt must raise.""" + ct = RecordCiphertext.from_string(_FOREIGN_CT) beacon_vk = ViewKey.from_string(_BEACON_VK) + foreign_vk = ViewKey.from_string(_FOREIGN_VK) assert ct.is_owner(beacon_vk) is False + assert ct.is_owner(foreign_vk) is False + with pytest.raises(RuntimeError): + ct.decrypt(foreign_vk) + with pytest.raises(RuntimeError): + ct.decrypt(beacon_vk) # --------------------------------------------------------------------------- From 2785b4fe773068f7ce5c4bc8bc07b2d205b812fa Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 06:18:53 -0400 Subject: [PATCH 34/72] feat: add hash algorithms module (BHP, Pedersen, Poseidon) with TS-SDK KATs Port 9 algorithm classes from ProvableHQ TS-SDK wasm bindings into sdk/src/algorithms/: BHP256/512/768/1024, Pedersen64/128, Poseidon2/4/8. Each class is a thin PyO3 wrapper over snarkvm::console::algorithms with hash, commit, and group/scalar projection methods. Also adds Field.to_bits_le() needed to construct BHP inputs. Includes 44 KAT tests against algebra.ts vectors verified against the TS-SDK reference implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 89 ++++++ sdk/python/aleo/_aleolib_mainnet.pyi | 89 ++++++ sdk/python/tests/test_algorithms.py | 341 +++++++++++++++++++++ sdk/python/tests/vectors/algorithms.json | 66 ++++ sdk/src/algebra/field.rs | 7 +- sdk/src/algorithms/bhp/bhp1024.rs | 65 ++++ sdk/src/algorithms/bhp/bhp256.rs | 65 ++++ sdk/src/algorithms/bhp/bhp512.rs | 65 ++++ sdk/src/algorithms/bhp/bhp768.rs | 65 ++++ sdk/src/algorithms/bhp/mod.rs | 27 ++ sdk/src/algorithms/mod.rs | 24 ++ sdk/src/algorithms/pedersen/mod.rs | 21 ++ sdk/src/algorithms/pedersen/pedersen128.rs | 60 ++++ sdk/src/algorithms/pedersen/pedersen64.rs | 60 ++++ sdk/src/algorithms/poseidon/mod.rs | 24 ++ sdk/src/algorithms/poseidon/poseidon2.rs | 71 +++++ sdk/src/algorithms/poseidon/poseidon4.rs | 71 +++++ sdk/src/algorithms/poseidon/poseidon8.rs | 71 +++++ sdk/src/lib.rs | 11 + 19 files changed, 1291 insertions(+), 1 deletion(-) create mode 100644 sdk/python/tests/test_algorithms.py create mode 100644 sdk/python/tests/vectors/algorithms.json create mode 100644 sdk/src/algorithms/bhp/bhp1024.rs create mode 100644 sdk/src/algorithms/bhp/bhp256.rs create mode 100644 sdk/src/algorithms/bhp/bhp512.rs create mode 100644 sdk/src/algorithms/bhp/bhp768.rs create mode 100644 sdk/src/algorithms/bhp/mod.rs create mode 100644 sdk/src/algorithms/mod.rs create mode 100644 sdk/src/algorithms/pedersen/mod.rs create mode 100644 sdk/src/algorithms/pedersen/pedersen128.rs create mode 100644 sdk/src/algorithms/pedersen/pedersen64.rs create mode 100644 sdk/src/algorithms/poseidon/mod.rs create mode 100644 sdk/src/algorithms/poseidon/poseidon2.rs create mode 100644 sdk/src/algorithms/poseidon/poseidon4.rs create mode 100644 sdk/src/algorithms/poseidon/poseidon8.rs diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index cb43e37..7394508 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -33,6 +33,46 @@ class Authorization: def bytes(self) -> bytes: ... +class BHP256: + def __new__(cls) -> BHP256: ... + @staticmethod + def setup(domain_separator: str) -> BHP256: ... + def hash(self, input: List[bool]) -> Field: ... + def hash_to_group(self, input: List[bool]) -> Group: ... + def commit(self, input: List[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: List[bool], randomizer: Scalar) -> Group: ... + + +class BHP512: + def __new__(cls) -> BHP512: ... + @staticmethod + def setup(domain_separator: str) -> BHP512: ... + def hash(self, input: List[bool]) -> Field: ... + def hash_to_group(self, input: List[bool]) -> Group: ... + def commit(self, input: List[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: List[bool], randomizer: Scalar) -> Group: ... + + +class BHP768: + def __new__(cls) -> BHP768: ... + @staticmethod + def setup(domain_separator: str) -> BHP768: ... + def hash(self, input: List[bool]) -> Field: ... + def hash_to_group(self, input: List[bool]) -> Group: ... + def commit(self, input: List[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: List[bool], randomizer: Scalar) -> Group: ... + + +class BHP1024: + def __new__(cls) -> BHP1024: ... + @staticmethod + def setup(domain_separator: str) -> BHP1024: ... + def hash(self, input: List[bool]) -> Field: ... + def hash_to_group(self, input: List[bool]) -> Group: ... + def commit(self, input: List[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: List[bool], randomizer: Scalar) -> Group: ... + + class Boolean: def __new__(cls, b: bool) -> Boolean: ... @@ -103,6 +143,7 @@ class Field: def zero() -> Field: ... def __mul__(self, other: Field) -> Field: ... def __truediv__(self, other: Field) -> Field: ... + def to_bits_le(self) -> List[bool]: ... class Group: @@ -218,6 +259,24 @@ class MicroCredits: def __int__(self) -> int: ... +class Pedersen64: + def __new__(cls) -> Pedersen64: ... + @staticmethod + def setup(domain_separator: str) -> Pedersen64: ... + def hash(self, input: List[bool]) -> Field: ... + def commit(self, input: List[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: List[bool], randomizer: Scalar) -> Group: ... + + +class Pedersen128: + def __new__(cls) -> Pedersen128: ... + @staticmethod + def setup(domain_separator: str) -> Pedersen128: ... + def hash(self, input: List[bool]) -> Field: ... + def commit(self, input: List[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: List[bool], randomizer: Scalar) -> Group: ... + + class Plaintext: @staticmethod def from_string(s: str) -> Plaintext: ... @@ -237,6 +296,36 @@ class Plaintext: def as_array(self) -> List[Plaintext]: ... +class Poseidon2: + def __new__(cls) -> Poseidon2: ... + @staticmethod + def setup(domain_separator: str) -> Poseidon2: ... + def hash(self, input: List[Field]) -> Field: ... + def hash_many(self, input: List[Field], num_outputs: int) -> List[Field]: ... + def hash_to_scalar(self, input: List[Field]) -> Scalar: ... + def hash_to_group(self, input: List[Field]) -> Group: ... + + +class Poseidon4: + def __new__(cls) -> Poseidon4: ... + @staticmethod + def setup(domain_separator: str) -> Poseidon4: ... + def hash(self, input: List[Field]) -> Field: ... + def hash_many(self, input: List[Field], num_outputs: int) -> List[Field]: ... + def hash_to_scalar(self, input: List[Field]) -> Scalar: ... + def hash_to_group(self, input: List[Field]) -> Group: ... + + +class Poseidon8: + def __new__(cls) -> Poseidon8: ... + @staticmethod + def setup(domain_separator: str) -> Poseidon8: ... + def hash(self, input: List[Field]) -> Field: ... + def hash_many(self, input: List[Field], num_outputs: int) -> List[Field]: ... + def hash_to_scalar(self, input: List[Field]) -> Scalar: ... + def hash_to_group(self, input: List[Field]) -> Group: ... + + class PrivateKey: @staticmethod def random() -> PrivateKey: ... diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index 656f72d..5d2af40 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -31,6 +31,46 @@ class Authorization: def bytes(self) -> bytes: ... +class BHP256: + def __new__(cls) -> BHP256: ... + @staticmethod + def setup(domain_separator: str) -> BHP256: ... + def hash(self, input: list[bool]) -> Field: ... + def hash_to_group(self, input: list[bool]) -> Group: ... + def commit(self, input: list[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: list[bool], randomizer: Scalar) -> Group: ... + + +class BHP512: + def __new__(cls) -> BHP512: ... + @staticmethod + def setup(domain_separator: str) -> BHP512: ... + def hash(self, input: list[bool]) -> Field: ... + def hash_to_group(self, input: list[bool]) -> Group: ... + def commit(self, input: list[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: list[bool], randomizer: Scalar) -> Group: ... + + +class BHP768: + def __new__(cls) -> BHP768: ... + @staticmethod + def setup(domain_separator: str) -> BHP768: ... + def hash(self, input: list[bool]) -> Field: ... + def hash_to_group(self, input: list[bool]) -> Group: ... + def commit(self, input: list[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: list[bool], randomizer: Scalar) -> Group: ... + + +class BHP1024: + def __new__(cls) -> BHP1024: ... + @staticmethod + def setup(domain_separator: str) -> BHP1024: ... + def hash(self, input: list[bool]) -> Field: ... + def hash_to_group(self, input: list[bool]) -> Group: ... + def commit(self, input: list[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: list[bool], randomizer: Scalar) -> Group: ... + + class Boolean: def __new__(cls, b: bool) -> Boolean: ... @@ -94,6 +134,7 @@ class Field: def zero() -> Field: ... def __mul__(self, other: Field) -> Field: ... def __truediv__(self, other: Field) -> Field: ... + def to_bits_le(self) -> list[bool]: ... class Group: @@ -209,6 +250,24 @@ class MicroCredits: def __int__(self) -> int: ... +class Pedersen64: + def __new__(cls) -> Pedersen64: ... + @staticmethod + def setup(domain_separator: str) -> Pedersen64: ... + def hash(self, input: list[bool]) -> Field: ... + def commit(self, input: list[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: list[bool], randomizer: Scalar) -> Group: ... + + +class Pedersen128: + def __new__(cls) -> Pedersen128: ... + @staticmethod + def setup(domain_separator: str) -> Pedersen128: ... + def hash(self, input: list[bool]) -> Field: ... + def commit(self, input: list[bool], randomizer: Scalar) -> Field: ... + def commit_to_group(self, input: list[bool], randomizer: Scalar) -> Group: ... + + class Plaintext: @staticmethod def from_string(s: str) -> Plaintext: ... @@ -228,6 +287,36 @@ class Plaintext: def as_array(self) -> list[Plaintext]: ... +class Poseidon2: + def __new__(cls) -> Poseidon2: ... + @staticmethod + def setup(domain_separator: str) -> Poseidon2: ... + def hash(self, input: list[Field]) -> Field: ... + def hash_many(self, input: list[Field], num_outputs: int) -> list[Field]: ... + def hash_to_scalar(self, input: list[Field]) -> Scalar: ... + def hash_to_group(self, input: list[Field]) -> Group: ... + + +class Poseidon4: + def __new__(cls) -> Poseidon4: ... + @staticmethod + def setup(domain_separator: str) -> Poseidon4: ... + def hash(self, input: list[Field]) -> Field: ... + def hash_many(self, input: list[Field], num_outputs: int) -> list[Field]: ... + def hash_to_scalar(self, input: list[Field]) -> Scalar: ... + def hash_to_group(self, input: list[Field]) -> Group: ... + + +class Poseidon8: + def __new__(cls) -> Poseidon8: ... + @staticmethod + def setup(domain_separator: str) -> Poseidon8: ... + def hash(self, input: list[Field]) -> Field: ... + def hash_many(self, input: list[Field], num_outputs: int) -> list[Field]: ... + def hash_to_scalar(self, input: list[Field]) -> Scalar: ... + def hash_to_group(self, input: list[Field]) -> Group: ... + + class PrivateKey: @staticmethod def random() -> PrivateKey: ... diff --git a/sdk/python/tests/test_algorithms.py b/sdk/python/tests/test_algorithms.py new file mode 100644 index 0000000..669af6c --- /dev/null +++ b/sdk/python/tests/test_algorithms.py @@ -0,0 +1,341 @@ +# Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +""" +Known-answer tests for hash algorithm classes (BHP, Pedersen, Poseidon). + +Source of KAT vectors: + github.com/ProvableHQ/sdk@543b41e0a7e3d6a8a49ae8d0870809dc92b88684 + sdk/tests/data/algebra.ts + sdk/tests/algorithm.test.ts +""" + +import json +import os +import pytest + +from aleo.mainnet import ( + BHP256, BHP512, BHP768, BHP1024, + Field, + Network, + Pedersen64, Pedersen128, + Poseidon2, Poseidon4, Poseidon8, + Scalar, +) + +# --------------------------------------------------------------------------- +# Load KAT vectors +# --------------------------------------------------------------------------- + +_VECTORS_PATH = os.path.join(os.path.dirname(__file__), "vectors", "algorithms.json") +with open(_VECTORS_PATH) as _f: + _V = json.load(_f) + +# --------------------------------------------------------------------------- +# Shared test inputs (mirrors algorithm.test.ts) +# --------------------------------------------------------------------------- + +# Field generator Fg and powers Fg², Fg³, Fg⁴ +_Fg = Field.from_string(_V["field_generator"]) +_F2 = _Fg * _Fg +_F3 = _F2 * _Fg +_F4 = _F3 * _Fg +_SFg = Scalar.from_string(_V["scalar_generator"]) + +# BHP / Pedersen input: concatenated to_bits_le of [Fg, F2, F3, F4] +_FIELD_BITS: list[bool] = ( + _Fg.to_bits_le() + + _F2.to_bits_le() + + _F3.to_bits_le() + + _F4.to_bits_le() +) + +# Poseidon input: list of field elements [Fg, F2, F3, F4] +_FIELD_ARRAY: list[Field] = [_Fg, _F2, _F3, _F4] + +# Pedersen input: two u32(1) LE → [T, F×31, T, F×31] (64 bits total) +_PEDERSEN_BITS: list[bool] = ( + [True] + [False] * 31 # u32(1) LE + + [True] + [False] * 31 # u32(1) LE +) + + +# --------------------------------------------------------------------------- +# Helper +# --------------------------------------------------------------------------- + +def _str(obj) -> str: + return str(obj) + + +# --------------------------------------------------------------------------- +# BHP256 +# --------------------------------------------------------------------------- + +class TestBHP256: + def setup_method(self): + self.hasher = BHP256() + + def test_hash(self): + result = self.hasher.hash(_FIELD_BITS) + assert _str(result) == _V["bhp256"]["hash"] + + def test_hash_to_group(self): + result = self.hasher.hash_to_group(_FIELD_BITS) + assert _str(result) == _V["bhp256"]["hash_to_group"] + + def test_commit(self): + result = self.hasher.commit(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp256"]["commit"] + + def test_commit_to_group(self): + result = self.hasher.commit_to_group(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp256"]["commit_to_group"] + + def test_custom_domain_differs(self): + custom = BHP256.setup("custom_domain_xyz") + default_hash = _str(self.hasher.hash(_FIELD_BITS)) + custom_hash = _str(custom.hash(_FIELD_BITS)) + assert default_hash != custom_hash + + +# --------------------------------------------------------------------------- +# BHP512 +# --------------------------------------------------------------------------- + +class TestBHP512: + def setup_method(self): + self.hasher = BHP512() + + def test_hash(self): + result = self.hasher.hash(_FIELD_BITS) + assert _str(result) == _V["bhp512"]["hash"] + + def test_hash_to_group(self): + result = self.hasher.hash_to_group(_FIELD_BITS) + assert _str(result) == _V["bhp512"]["hash_to_group"] + + def test_commit(self): + result = self.hasher.commit(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp512"]["commit"] + + def test_commit_to_group(self): + result = self.hasher.commit_to_group(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp512"]["commit_to_group"] + + def test_custom_domain_differs(self): + custom = BHP512.setup("custom_domain_xyz") + assert _str(self.hasher.hash(_FIELD_BITS)) != _str(custom.hash(_FIELD_BITS)) + + +# --------------------------------------------------------------------------- +# BHP768 +# --------------------------------------------------------------------------- + +class TestBHP768: + def setup_method(self): + self.hasher = BHP768() + + def test_hash(self): + result = self.hasher.hash(_FIELD_BITS) + assert _str(result) == _V["bhp768"]["hash"] + + def test_hash_to_group(self): + result = self.hasher.hash_to_group(_FIELD_BITS) + assert _str(result) == _V["bhp768"]["hash_to_group"] + + def test_commit(self): + result = self.hasher.commit(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp768"]["commit"] + + def test_commit_to_group(self): + result = self.hasher.commit_to_group(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp768"]["commit_to_group"] + + def test_custom_domain_differs(self): + custom = BHP768.setup("custom_domain_xyz") + assert _str(self.hasher.hash(_FIELD_BITS)) != _str(custom.hash(_FIELD_BITS)) + + +# --------------------------------------------------------------------------- +# BHP1024 +# --------------------------------------------------------------------------- + +class TestBHP1024: + def setup_method(self): + self.hasher = BHP1024() + + def test_hash(self): + result = self.hasher.hash(_FIELD_BITS) + assert _str(result) == _V["bhp1024"]["hash"] + + def test_hash_to_group(self): + result = self.hasher.hash_to_group(_FIELD_BITS) + assert _str(result) == _V["bhp1024"]["hash_to_group"] + + def test_commit(self): + result = self.hasher.commit(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp1024"]["commit"] + + def test_commit_to_group(self): + result = self.hasher.commit_to_group(_FIELD_BITS, _SFg) + assert _str(result) == _V["bhp1024"]["commit_to_group"] + + def test_custom_domain_differs(self): + custom = BHP1024.setup("custom_domain_xyz") + assert _str(self.hasher.hash(_FIELD_BITS)) != _str(custom.hash(_FIELD_BITS)) + + +# --------------------------------------------------------------------------- +# Pedersen64 +# --------------------------------------------------------------------------- + +class TestPedersen64: + def setup_method(self): + self.hasher = Pedersen64() + + def test_hash(self): + result = self.hasher.hash(_PEDERSEN_BITS) + assert _str(result) == _V["pedersen64"]["hash"] + + def test_commit(self): + result = self.hasher.commit(_PEDERSEN_BITS, _SFg) + assert _str(result) == _V["pedersen64"]["commit"] + + def test_commit_to_group(self): + result = self.hasher.commit_to_group(_PEDERSEN_BITS, _SFg) + assert _str(result) == _V["pedersen64"]["commit_to_group"] + + def test_custom_domain_differs(self): + custom = Pedersen64.setup("custom_domain_xyz") + assert _str(self.hasher.hash(_PEDERSEN_BITS)) != _str(custom.hash(_PEDERSEN_BITS)) + + +# --------------------------------------------------------------------------- +# Pedersen128 +# --------------------------------------------------------------------------- + +class TestPedersen128: + def setup_method(self): + self.hasher = Pedersen128() + + def test_hash(self): + result = self.hasher.hash(_PEDERSEN_BITS) + assert _str(result) == _V["pedersen128"]["hash"] + + def test_commit(self): + result = self.hasher.commit(_PEDERSEN_BITS, _SFg) + assert _str(result) == _V["pedersen128"]["commit"] + + def test_commit_to_group(self): + result = self.hasher.commit_to_group(_PEDERSEN_BITS, _SFg) + assert _str(result) == _V["pedersen128"]["commit_to_group"] + + def test_custom_domain_differs(self): + custom = Pedersen128.setup("custom_domain_xyz") + assert _str(self.hasher.hash(_PEDERSEN_BITS)) != _str(custom.hash(_PEDERSEN_BITS)) + + +# --------------------------------------------------------------------------- +# Poseidon2 +# --------------------------------------------------------------------------- + +class TestPoseidon2: + def setup_method(self): + self.hasher = Poseidon2() + + def test_hash(self): + result = self.hasher.hash(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon2"]["hash"] + + def test_hash_to_scalar(self): + result = self.hasher.hash_to_scalar(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon2"]["hash_to_scalar"] + + def test_hash_to_group(self): + result = self.hasher.hash_to_group(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon2"]["hash_to_group"] + + def test_hash_many(self): + results = self.hasher.hash_many(list(_FIELD_ARRAY), 2) + assert [_str(r) for r in results] == _V["poseidon2"]["hash_many_2"] + + def test_custom_domain_differs(self): + custom = Poseidon2.setup("custom_domain_xyz") + assert _str(self.hasher.hash(list(_FIELD_ARRAY))) != _str(custom.hash(list(_FIELD_ARRAY))) + + def test_equals_network_hash_psd2(self): + """Poseidon2.hash must match Network.hash_psd2 (cross-check).""" + expected = Network.hash_psd2(_FIELD_ARRAY) + result = self.hasher.hash(list(_FIELD_ARRAY)) + assert _str(result) == _str(expected) + + +# --------------------------------------------------------------------------- +# Poseidon4 +# --------------------------------------------------------------------------- + +class TestPoseidon4: + def setup_method(self): + self.hasher = Poseidon4() + + def test_hash(self): + result = self.hasher.hash(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon4"]["hash"] + + def test_hash_to_scalar(self): + result = self.hasher.hash_to_scalar(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon4"]["hash_to_scalar"] + + def test_hash_to_group(self): + result = self.hasher.hash_to_group(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon4"]["hash_to_group"] + + def test_hash_many(self): + results = self.hasher.hash_many(list(_FIELD_ARRAY), 2) + assert [_str(r) for r in results] == _V["poseidon4"]["hash_many_2"] + + def test_custom_domain_differs(self): + custom = Poseidon4.setup("custom_domain_xyz") + assert _str(self.hasher.hash(list(_FIELD_ARRAY))) != _str(custom.hash(list(_FIELD_ARRAY))) + + +# --------------------------------------------------------------------------- +# Poseidon8 +# --------------------------------------------------------------------------- + +class TestPoseidon8: + def setup_method(self): + self.hasher = Poseidon8() + + def test_hash(self): + result = self.hasher.hash(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon8"]["hash"] + + def test_hash_to_scalar(self): + result = self.hasher.hash_to_scalar(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon8"]["hash_to_scalar"] + + def test_hash_to_group(self): + result = self.hasher.hash_to_group(list(_FIELD_ARRAY)) + assert _str(result) == _V["poseidon8"]["hash_to_group"] + + def test_hash_many(self): + results = self.hasher.hash_many(list(_FIELD_ARRAY), 2) + assert [_str(r) for r in results] == _V["poseidon8"]["hash_many_2"] + + def test_custom_domain_differs(self): + custom = Poseidon8.setup("custom_domain_xyz") + assert _str(self.hasher.hash(list(_FIELD_ARRAY))) != _str(custom.hash(list(_FIELD_ARRAY))) diff --git a/sdk/python/tests/vectors/algorithms.json b/sdk/python/tests/vectors/algorithms.json new file mode 100644 index 0000000..8b37c14 --- /dev/null +++ b/sdk/python/tests/vectors/algorithms.json @@ -0,0 +1,66 @@ +{ + "_source": "github.com/ProvableHQ/sdk@543b41e0a7e3d6a8a49ae8d0870809dc92b88684 sdk/tests/data/algebra.ts", + "field_generator": "6901184695964460143517399399785179769303979738604374595034454667750561389951field", + "scalar_generator": "1774157567936692047646837016039369013254365378639847034769080448564598011047scalar", + "bhp256": { + "hash": "6598783590400646668520287182111940745128736688662567560889280298782004590744field", + "hash_to_group": "6598783590400646668520287182111940745128736688662567560889280298782004590744group", + "commit": "2082593229082262381320813637490421437118190271647234845496709605556536239325field", + "commit_to_group": "2082593229082262381320813637490421437118190271647234845496709605556536239325group" + }, + "bhp512": { + "hash": "1999822247055455701784132575168585963242270924818325055212777584340934143916field", + "hash_to_group": "1999822247055455701784132575168585963242270924818325055212777584340934143916group", + "commit": "5702389816400555524923367597422208802601150072230907483584911172290469121501field", + "commit_to_group": "5702389816400555524923367597422208802601150072230907483584911172290469121501group" + }, + "bhp768": { + "hash": "2151969465368594987252495420983687091399796648584310972463234119446869285487field", + "hash_to_group": "2151969465368594987252495420983687091399796648584310972463234119446869285487group", + "commit": "6864405450086290602821044466228181619883104706596830134315601499052844411477field", + "commit_to_group": "6864405450086290602821044466228181619883104706596830134315601499052844411477group" + }, + "bhp1024": { + "hash": "5012718612752062270473753102141489376962182387590529397014861985202772382691field", + "hash_to_group": "5012718612752062270473753102141489376962182387590529397014861985202772382691group", + "commit": "7355825599810198751875041563628058158258329601754296576667945643687737135311field", + "commit_to_group": "7355825599810198751875041563628058158258329601754296576667945643687737135311group" + }, + "pedersen64": { + "hash": "7641648045329738245569851934154954931983769745339084901557923877960530097536field", + "commit": "4214712642932384902492032091870474319129850239541860097363932997605707681866field", + "commit_to_group": "4214712642932384902492032091870474319129850239541860097363932997605707681866group" + }, + "pedersen128": { + "hash": "2635650346747854690304767890512510318865900869251739129887484152040487384946field", + "commit": "2510571858482041100602183741907011481539483352792333201599869098117502813920field", + "commit_to_group": "2510571858482041100602183741907011481539483352792333201599869098117502813920group" + }, + "poseidon2": { + "hash": "5077032915756006405056357976663159304886914340125619713231037384461532417432field", + "hash_to_scalar": "1458530127089875298069764695141662184472227319324352086997987884214247116184scalar", + "hash_to_group": "6725009305095024168741689761204333975914699402177819232581771014527031061395group", + "hash_many_2": [ + "5077032915756006405056357976663159304886914340125619713231037384461532417432field", + "7556882202382565798668668067123114920984638418911135511310139766017967865048field" + ] + }, + "poseidon4": { + "hash": "7634545961811588936484713301052408690645763622397315677026301158133701966262field", + "hash_to_scalar": "397540384479326722511526738009414449816389580794780424560202157639131363766scalar", + "hash_to_group": "3501730277828187397862093302382961412852320364543135222391952713144715771762group", + "hash_many_2": [ + "7634545961811588936484713301052408690645763622397315677026301158133701966262field", + "6897041872403455461974187270360400367782809654597486212257448618986966168076field" + ] + }, + "poseidon8": { + "hash": "491310798299229303453029190751493384624125693265313448104421146921465784609field", + "hash_to_scalar": "491310798299229303453029190751493384624125693265313448104421146921465784609scalar", + "hash_to_group": "7639970001763982540831449531477540915761314714085721616177080725803487493174group", + "hash_many_2": [ + "491310798299229303453029190751493384624125693265313448104421146921465784609field", + "476244868544635089532724552855423418881253978175875838088194622836984800804field" + ] + } +} diff --git a/sdk/src/algebra/field.rs b/sdk/src/algebra/field.rs index a515caf..8500e97 100644 --- a/sdk/src/algebra/field.rs +++ b/sdk/src/algebra/field.rs @@ -18,7 +18,7 @@ use crate::types::FieldNative; use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; use rand::rngs::StdRng; -use snarkvm::prelude::{Uniform, Zero}; +use snarkvm::prelude::{ToBits, Uniform, Zero}; use std::{ collections::hash_map::DefaultHasher, @@ -90,6 +90,11 @@ impl Field { self.0.hash(&mut hasher); hasher.finish() } + + /// Returns the little-endian bit representation of the field element. + pub fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } } impl Deref for Field { diff --git a/sdk/src/algorithms/bhp/bhp1024.rs b/sdk/src/algorithms/bhp/bhp1024.rs new file mode 100644 index 0000000..b3a2c7d --- /dev/null +++ b/sdk/src/algorithms/bhp/bhp1024.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::CurrentNetwork, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::BHP1024 as BHP1024Native; +use snarkvm::prelude::{Commit, CommitUncompressed, Hash, HashUncompressed}; + +/// BHP1024 is a collision-resistant hash function that processes 1024-bit inputs. +#[pyclass(frozen)] +pub struct BHP1024(BHP1024Native<CurrentNetwork>); + +#[pymethods] +impl BHP1024 { + /// Creates a BHP1024 hasher with the default domain separator "AleoBHP1024". + #[new] + pub fn new() -> anyhow::Result<Self> { + BHP1024Native::setup("AleoBHP1024").map(Self) + } + + /// Creates a BHP1024 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> anyhow::Result<Self> { + BHP1024Native::setup(domain_separator).map(Self) + } + + /// Returns the BHP1024 hash of the given bit-string input. + pub fn hash(&self, input: Vec<bool>) -> anyhow::Result<Field> { + self.0.hash(&input).map(Field::from) + } + + /// Returns the BHP1024 hash of the given input as a group element. + pub fn hash_to_group(&self, input: Vec<bool>) -> anyhow::Result<Group> { + self.0.hash_uncompressed(&input).map(Group::from) + } + + /// Returns a BHP1024 commitment for the given input and randomizer. + pub fn commit(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Field> { + self.0.commit(&input, &*randomizer).map(Field::from) + } + + /// Returns a BHP1024 commitment for the given input and randomizer as a group element. + pub fn commit_to_group(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Group> { + self.0 + .commit_uncompressed(&input, &*randomizer) + .map(Group::from) + } +} diff --git a/sdk/src/algorithms/bhp/bhp256.rs b/sdk/src/algorithms/bhp/bhp256.rs new file mode 100644 index 0000000..555ea7d --- /dev/null +++ b/sdk/src/algorithms/bhp/bhp256.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::CurrentNetwork, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::BHP256 as BHP256Native; +use snarkvm::prelude::{Commit, CommitUncompressed, Hash, HashUncompressed}; + +/// BHP256 is a collision-resistant hash function that processes 256-bit inputs. +#[pyclass(frozen)] +pub struct BHP256(BHP256Native<CurrentNetwork>); + +#[pymethods] +impl BHP256 { + /// Creates a BHP256 hasher with the default domain separator "AleoBHP256". + #[new] + pub fn new() -> anyhow::Result<Self> { + BHP256Native::setup("AleoBHP256").map(Self) + } + + /// Creates a BHP256 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> anyhow::Result<Self> { + BHP256Native::setup(domain_separator).map(Self) + } + + /// Returns the BHP256 hash of the given bit-string input. + pub fn hash(&self, input: Vec<bool>) -> anyhow::Result<Field> { + self.0.hash(&input).map(Field::from) + } + + /// Returns the BHP256 hash of the given input as a group element. + pub fn hash_to_group(&self, input: Vec<bool>) -> anyhow::Result<Group> { + self.0.hash_uncompressed(&input).map(Group::from) + } + + /// Returns a BHP256 commitment for the given input and randomizer. + pub fn commit(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Field> { + self.0.commit(&input, &*randomizer).map(Field::from) + } + + /// Returns a BHP256 commitment for the given input and randomizer as a group element. + pub fn commit_to_group(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Group> { + self.0 + .commit_uncompressed(&input, &*randomizer) + .map(Group::from) + } +} diff --git a/sdk/src/algorithms/bhp/bhp512.rs b/sdk/src/algorithms/bhp/bhp512.rs new file mode 100644 index 0000000..b1ddea2 --- /dev/null +++ b/sdk/src/algorithms/bhp/bhp512.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::CurrentNetwork, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::BHP512 as BHP512Native; +use snarkvm::prelude::{Commit, CommitUncompressed, Hash, HashUncompressed}; + +/// BHP512 is a collision-resistant hash function that processes 512-bit inputs. +#[pyclass(frozen)] +pub struct BHP512(BHP512Native<CurrentNetwork>); + +#[pymethods] +impl BHP512 { + /// Creates a BHP512 hasher with the default domain separator "AleoBHP512". + #[new] + pub fn new() -> anyhow::Result<Self> { + BHP512Native::setup("AleoBHP512").map(Self) + } + + /// Creates a BHP512 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> anyhow::Result<Self> { + BHP512Native::setup(domain_separator).map(Self) + } + + /// Returns the BHP512 hash of the given bit-string input. + pub fn hash(&self, input: Vec<bool>) -> anyhow::Result<Field> { + self.0.hash(&input).map(Field::from) + } + + /// Returns the BHP512 hash of the given input as a group element. + pub fn hash_to_group(&self, input: Vec<bool>) -> anyhow::Result<Group> { + self.0.hash_uncompressed(&input).map(Group::from) + } + + /// Returns a BHP512 commitment for the given input and randomizer. + pub fn commit(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Field> { + self.0.commit(&input, &*randomizer).map(Field::from) + } + + /// Returns a BHP512 commitment for the given input and randomizer as a group element. + pub fn commit_to_group(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Group> { + self.0 + .commit_uncompressed(&input, &*randomizer) + .map(Group::from) + } +} diff --git a/sdk/src/algorithms/bhp/bhp768.rs b/sdk/src/algorithms/bhp/bhp768.rs new file mode 100644 index 0000000..e2c313d --- /dev/null +++ b/sdk/src/algorithms/bhp/bhp768.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::CurrentNetwork, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::BHP768 as BHP768Native; +use snarkvm::prelude::{Commit, CommitUncompressed, Hash, HashUncompressed}; + +/// BHP768 is a collision-resistant hash function that processes 768-bit inputs. +#[pyclass(frozen)] +pub struct BHP768(BHP768Native<CurrentNetwork>); + +#[pymethods] +impl BHP768 { + /// Creates a BHP768 hasher with the default domain separator "AleoBHP768". + #[new] + pub fn new() -> anyhow::Result<Self> { + BHP768Native::setup("AleoBHP768").map(Self) + } + + /// Creates a BHP768 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> anyhow::Result<Self> { + BHP768Native::setup(domain_separator).map(Self) + } + + /// Returns the BHP768 hash of the given bit-string input. + pub fn hash(&self, input: Vec<bool>) -> anyhow::Result<Field> { + self.0.hash(&input).map(Field::from) + } + + /// Returns the BHP768 hash of the given input as a group element. + pub fn hash_to_group(&self, input: Vec<bool>) -> anyhow::Result<Group> { + self.0.hash_uncompressed(&input).map(Group::from) + } + + /// Returns a BHP768 commitment for the given input and randomizer. + pub fn commit(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Field> { + self.0.commit(&input, &*randomizer).map(Field::from) + } + + /// Returns a BHP768 commitment for the given input and randomizer as a group element. + pub fn commit_to_group(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Group> { + self.0 + .commit_uncompressed(&input, &*randomizer) + .map(Group::from) + } +} diff --git a/sdk/src/algorithms/bhp/mod.rs b/sdk/src/algorithms/bhp/mod.rs new file mode 100644 index 0000000..ce48d60 --- /dev/null +++ b/sdk/src/algorithms/bhp/mod.rs @@ -0,0 +1,27 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +mod bhp256; +pub use bhp256::BHP256; + +mod bhp512; +pub use bhp512::BHP512; + +mod bhp768; +pub use bhp768::BHP768; + +mod bhp1024; +pub use bhp1024::BHP1024; diff --git a/sdk/src/algorithms/mod.rs b/sdk/src/algorithms/mod.rs new file mode 100644 index 0000000..bafeae7 --- /dev/null +++ b/sdk/src/algorithms/mod.rs @@ -0,0 +1,24 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +pub mod bhp; +pub use bhp::{BHP1024, BHP256, BHP512, BHP768}; + +pub mod pedersen; +pub use pedersen::{Pedersen128, Pedersen64}; + +pub mod poseidon; +pub use poseidon::{Poseidon2, Poseidon4, Poseidon8}; diff --git a/sdk/src/algorithms/pedersen/mod.rs b/sdk/src/algorithms/pedersen/mod.rs new file mode 100644 index 0000000..7cfccf5 --- /dev/null +++ b/sdk/src/algorithms/pedersen/mod.rs @@ -0,0 +1,21 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +mod pedersen64; +pub use pedersen64::Pedersen64; + +mod pedersen128; +pub use pedersen128::Pedersen128; diff --git a/sdk/src/algorithms/pedersen/pedersen128.rs b/sdk/src/algorithms/pedersen/pedersen128.rs new file mode 100644 index 0000000..999aca7 --- /dev/null +++ b/sdk/src/algorithms/pedersen/pedersen128.rs @@ -0,0 +1,60 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::CurrentNetwork, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::Pedersen128 as Pedersen128Native; +use snarkvm::prelude::{Commit, CommitUncompressed, Hash}; + +/// Pedersen128 is an additively-homomorphic collision-resistant hash function for up to 128-bit inputs. +#[pyclass(frozen)] +pub struct Pedersen128(Pedersen128Native<CurrentNetwork>); + +#[pymethods] +impl Pedersen128 { + /// Creates a Pedersen128 hasher with the default domain separator "AleoPedersen128". + #[new] + pub fn new() -> Self { + Self(Pedersen128Native::setup("AleoPedersen128")) + } + + /// Creates a Pedersen128 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> Self { + Self(Pedersen128Native::setup(domain_separator)) + } + + /// Returns the Pedersen128 hash of the given bit-string input. + pub fn hash(&self, input: Vec<bool>) -> anyhow::Result<Field> { + self.0.hash(&input).map(Field::from) + } + + /// Returns a Pedersen128 commitment for the given input and randomizer. + pub fn commit(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Field> { + self.0.commit(&input, &*randomizer).map(Field::from) + } + + /// Returns a Pedersen128 commitment for the given input and randomizer as a group element. + pub fn commit_to_group(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Group> { + self.0 + .commit_uncompressed(&input, &*randomizer) + .map(Group::from) + } +} diff --git a/sdk/src/algorithms/pedersen/pedersen64.rs b/sdk/src/algorithms/pedersen/pedersen64.rs new file mode 100644 index 0000000..9107301 --- /dev/null +++ b/sdk/src/algorithms/pedersen/pedersen64.rs @@ -0,0 +1,60 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::CurrentNetwork, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::Pedersen64 as Pedersen64Native; +use snarkvm::prelude::{Commit, CommitUncompressed, Hash}; + +/// Pedersen64 is an additively-homomorphic collision-resistant hash function for up to 64-bit inputs. +#[pyclass(frozen)] +pub struct Pedersen64(Pedersen64Native<CurrentNetwork>); + +#[pymethods] +impl Pedersen64 { + /// Creates a Pedersen64 hasher with the default domain separator "AleoPedersen64". + #[new] + pub fn new() -> Self { + Self(Pedersen64Native::setup("AleoPedersen64")) + } + + /// Creates a Pedersen64 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> Self { + Self(Pedersen64Native::setup(domain_separator)) + } + + /// Returns the Pedersen64 hash of the given bit-string input. + pub fn hash(&self, input: Vec<bool>) -> anyhow::Result<Field> { + self.0.hash(&input).map(Field::from) + } + + /// Returns a Pedersen64 commitment for the given input and randomizer. + pub fn commit(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Field> { + self.0.commit(&input, &*randomizer).map(Field::from) + } + + /// Returns a Pedersen64 commitment for the given input and randomizer as a group element. + pub fn commit_to_group(&self, input: Vec<bool>, randomizer: Scalar) -> anyhow::Result<Group> { + self.0 + .commit_uncompressed(&input, &*randomizer) + .map(Group::from) + } +} diff --git a/sdk/src/algorithms/poseidon/mod.rs b/sdk/src/algorithms/poseidon/mod.rs new file mode 100644 index 0000000..5817d5b --- /dev/null +++ b/sdk/src/algorithms/poseidon/mod.rs @@ -0,0 +1,24 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +mod poseidon2; +pub use poseidon2::Poseidon2; + +mod poseidon4; +pub use poseidon4::Poseidon4; + +mod poseidon8; +pub use poseidon8::Poseidon8; diff --git a/sdk/src/algorithms/poseidon/poseidon2.rs b/sdk/src/algorithms/poseidon/poseidon2.rs new file mode 100644 index 0000000..e2c414d --- /dev/null +++ b/sdk/src/algorithms/poseidon/poseidon2.rs @@ -0,0 +1,71 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::{CurrentNetwork, FieldNative}, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::Poseidon2 as Poseidon2Native; +use snarkvm::prelude::{Hash, HashMany, HashToGroup, HashToScalar}; + +/// Poseidon2 is a cryptographic hash function with an input rate of 2. +#[pyclass(frozen)] +pub struct Poseidon2(Poseidon2Native<CurrentNetwork>); + +#[pymethods] +impl Poseidon2 { + /// Creates a Poseidon2 hasher with the default domain separator "AleoPoseidon2". + #[new] + pub fn new() -> anyhow::Result<Self> { + Poseidon2Native::setup("AleoPoseidon2").map(Self) + } + + /// Creates a Poseidon2 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> anyhow::Result<Self> { + Poseidon2Native::setup(domain_separator).map(Self) + } + + /// Returns the Poseidon2 hash of the given field elements. + pub fn hash(&self, input: Vec<Field>) -> anyhow::Result<Field> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash(&input).map(Field::from) + } + + /// Returns multiple Poseidon2 hash outputs for the given field elements. + pub fn hash_many(&self, input: Vec<Field>, num_outputs: u16) -> Vec<Field> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0 + .hash_many(&input, num_outputs) + .into_iter() + .map(Field::from) + .collect() + } + + /// Returns the Poseidon2 hash of the given field elements projected to a scalar. + pub fn hash_to_scalar(&self, input: Vec<Field>) -> anyhow::Result<Scalar> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash_to_scalar(&input).map(Scalar::from) + } + + /// Returns the Poseidon2 hash of the given field elements projected to a group element. + pub fn hash_to_group(&self, input: Vec<Field>) -> anyhow::Result<Group> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash_to_group(&input).map(Group::from) + } +} diff --git a/sdk/src/algorithms/poseidon/poseidon4.rs b/sdk/src/algorithms/poseidon/poseidon4.rs new file mode 100644 index 0000000..7c5c21a --- /dev/null +++ b/sdk/src/algorithms/poseidon/poseidon4.rs @@ -0,0 +1,71 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::{CurrentNetwork, FieldNative}, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::Poseidon4 as Poseidon4Native; +use snarkvm::prelude::{Hash, HashMany, HashToGroup, HashToScalar}; + +/// Poseidon4 is a cryptographic hash function with an input rate of 4. +#[pyclass(frozen)] +pub struct Poseidon4(Poseidon4Native<CurrentNetwork>); + +#[pymethods] +impl Poseidon4 { + /// Creates a Poseidon4 hasher with the default domain separator "AleoPoseidon4". + #[new] + pub fn new() -> anyhow::Result<Self> { + Poseidon4Native::setup("AleoPoseidon4").map(Self) + } + + /// Creates a Poseidon4 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> anyhow::Result<Self> { + Poseidon4Native::setup(domain_separator).map(Self) + } + + /// Returns the Poseidon4 hash of the given field elements. + pub fn hash(&self, input: Vec<Field>) -> anyhow::Result<Field> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash(&input).map(Field::from) + } + + /// Returns multiple Poseidon4 hash outputs for the given field elements. + pub fn hash_many(&self, input: Vec<Field>, num_outputs: u16) -> Vec<Field> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0 + .hash_many(&input, num_outputs) + .into_iter() + .map(Field::from) + .collect() + } + + /// Returns the Poseidon4 hash of the given field elements projected to a scalar. + pub fn hash_to_scalar(&self, input: Vec<Field>) -> anyhow::Result<Scalar> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash_to_scalar(&input).map(Scalar::from) + } + + /// Returns the Poseidon4 hash of the given field elements projected to a group element. + pub fn hash_to_group(&self, input: Vec<Field>) -> anyhow::Result<Group> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash_to_group(&input).map(Group::from) + } +} diff --git a/sdk/src/algorithms/poseidon/poseidon8.rs b/sdk/src/algorithms/poseidon/poseidon8.rs new file mode 100644 index 0000000..2b90cfe --- /dev/null +++ b/sdk/src/algorithms/poseidon/poseidon8.rs @@ -0,0 +1,71 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + algebra::{Field, Group, Scalar}, + types::{CurrentNetwork, FieldNative}, +}; + +use pyo3::prelude::*; +use snarkvm::console::algorithms::Poseidon8 as Poseidon8Native; +use snarkvm::prelude::{Hash, HashMany, HashToGroup, HashToScalar}; + +/// Poseidon8 is a cryptographic hash function with an input rate of 8. +#[pyclass(frozen)] +pub struct Poseidon8(Poseidon8Native<CurrentNetwork>); + +#[pymethods] +impl Poseidon8 { + /// Creates a Poseidon8 hasher with the default domain separator "AleoPoseidon8". + #[new] + pub fn new() -> anyhow::Result<Self> { + Poseidon8Native::setup("AleoPoseidon8").map(Self) + } + + /// Creates a Poseidon8 hasher with a custom domain separator. + #[staticmethod] + pub fn setup(domain_separator: &str) -> anyhow::Result<Self> { + Poseidon8Native::setup(domain_separator).map(Self) + } + + /// Returns the Poseidon8 hash of the given field elements. + pub fn hash(&self, input: Vec<Field>) -> anyhow::Result<Field> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash(&input).map(Field::from) + } + + /// Returns multiple Poseidon8 hash outputs for the given field elements. + pub fn hash_many(&self, input: Vec<Field>, num_outputs: u16) -> Vec<Field> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0 + .hash_many(&input, num_outputs) + .into_iter() + .map(Field::from) + .collect() + } + + /// Returns the Poseidon8 hash of the given field elements projected to a scalar. + pub fn hash_to_scalar(&self, input: Vec<Field>) -> anyhow::Result<Scalar> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash_to_scalar(&input).map(Scalar::from) + } + + /// Returns the Poseidon8 hash of the given field elements projected to a group element. + pub fn hash_to_group(&self, input: Vec<Field>) -> anyhow::Result<Group> { + let input: Vec<FieldNative> = input.into_iter().map(FieldNative::from).collect(); + self.0.hash_to_group(&input).map(Group::from) + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 1dffadd..d7d66f9 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -21,6 +21,7 @@ use pyo3::prelude::*; mod account; mod algebra; +mod algorithms; mod credits; mod network; mod programs; @@ -28,6 +29,7 @@ mod types; use account::*; use algebra::*; +use algorithms::*; use credits::*; use network::*; use programs::*; @@ -40,6 +42,10 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Account>()?; m.add_class::<Address>()?; m.add_class::<Authorization>()?; + m.add_class::<BHP256>()?; + m.add_class::<BHP512>()?; + m.add_class::<BHP768>()?; + m.add_class::<BHP1024>()?; m.add_class::<Boolean>()?; m.add_class::<Ciphertext>()?; m.add_class::<ComputeKey>()?; @@ -58,7 +64,12 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Locator>()?; m.add_class::<MicroCredits>()?; m.add_class::<Network>()?; + m.add_class::<Pedersen64>()?; + m.add_class::<Pedersen128>()?; m.add_class::<Plaintext>()?; + m.add_class::<Poseidon2>()?; + m.add_class::<Poseidon4>()?; + m.add_class::<Poseidon8>()?; m.add_class::<PrivateKey>()?; m.add_class::<Process>()?; m.add_class::<Program>()?; From 3b391bc9f9e8ea2a4600c0fe8d402300b20bfa4e Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 07:00:05 -0400 Subject: [PATCH 35/72] =?UTF-8?q?feat:=20record-scanning=20parity=20?= =?UTF-8?q?=E2=80=94=20GraphKey,=20record=20commitments/tags/RVK,=20group/?= =?UTF-8?q?field/scalar=20arithmetic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 82 +++++ sdk/python/aleo/_aleolib_mainnet.pyi | 82 +++++ sdk/python/tests/test_records_extra.py | 409 +++++++++++++++++++++++++ sdk/src/account/address.rs | 40 ++- sdk/src/account/graph_key.rs | 87 ++++++ sdk/src/account/mod.rs | 3 + sdk/src/account/record.rs | 104 ++++++- sdk/src/account/view_key.rs | 13 +- sdk/src/algebra/field.rs | 103 ++++++- sdk/src/algebra/group.rs | 88 +++++- sdk/src/algebra/scalar.rs | 70 ++++- sdk/src/lib.rs | 1 + sdk/src/types.rs | 9 +- 13 files changed, 1067 insertions(+), 24 deletions(-) create mode 100644 sdk/python/tests/test_records_extra.py create mode 100644 sdk/src/account/graph_key.rs diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index 7394508..6c24087 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -22,6 +22,13 @@ class Account: class Address: @staticmethod def from_string(s: str) -> Address: ... + @staticmethod + def from_group(group: Group) -> Address: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Address: ... + def to_group(self) -> Group: ... + def to_field(self) -> Field: ... + def to_bytes_le(self) -> bytes: ... class Authorization: @@ -141,16 +148,63 @@ class Field: def from_u128(u128: int) -> Field: ... @staticmethod def zero() -> Field: ... + @staticmethod + def one() -> Field: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> Field: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Field: ... + def to_bytes_le(self) -> bytes: ... + def double(self) -> Field: ... + def add(self, other: Field) -> Field: ... + def subtract(self, other: Field) -> Field: ... + def multiply(self, other: Field) -> Field: ... + def divide(self, other: Field) -> Field: ... + def pow(self, other: Field) -> Field: ... + def negate(self) -> Field: ... + def to_scalar_lossy(self) -> Scalar: ... + def to_group(self) -> Group: ... + def __add__(self, other: Field) -> Field: ... + def __sub__(self, other: Field) -> Field: ... def __mul__(self, other: Field) -> Field: ... def __truediv__(self, other: Field) -> Field: ... + def __pow__(self, other: Field) -> Field: ... + def __neg__(self) -> Field: ... def to_bits_le(self) -> List[bool]: ... +class GraphKey: + @staticmethod + def from_view_key(view_key: ViewKey) -> GraphKey: ... + @staticmethod + def from_string(s: str) -> GraphKey: ... + @property + def sk_tag(self) -> Field: ... + + class Group: @staticmethod def from_string(s: str) -> Group: ... @staticmethod def zero() -> Group: ... + @staticmethod + def generator() -> Group: ... + @staticmethod + def from_field(field: Field) -> Group: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Group: ... + def to_x_coordinate(self) -> Field: ... + def to_address(self) -> Address: ... + def double(self) -> Group: ... + def to_bytes_le(self) -> bytes: ... + def add(self, other: Group) -> Group: ... + def subtract(self, other: Group) -> Group: ... + def negate(self) -> Group: ... + def scalar_multiply(self, scalar: Scalar) -> Group: ... + def __add__(self, other: Group) -> Group: ... + def __sub__(self, other: Group) -> Group: ... + def __neg__(self) -> Group: ... + def __mul__(self, scalar: Scalar) -> Group: ... class Identifier: @@ -410,6 +464,12 @@ class RecordCiphertext: def from_string(s: str) -> RecordCiphertext: ... def decrypt(self, view_key: ViewKey) -> RecordPlaintext: ... def is_owner(self, view_key: ViewKey) -> bool: ... + @property + def nonce(self) -> Group: ... + def record_view_key(self, view_key: ViewKey) -> Field: ... + def decrypt_with_record_view_key(self, record_view_key: Field) -> RecordPlaintext: ... + @staticmethod + def tag(graph_key: GraphKey, commitment: Field) -> Field: ... class RecordPlaintext: @@ -421,6 +481,12 @@ class RecordPlaintext: def nonce(self) -> Group: ... @property def version(self) -> int: ... + @property + def microcredits(self) -> int: ... + def commitment(self, program_id: ProgramID, record_name: Identifier, record_view_key: Field) -> Field: ... + def record_view_key(self, view_key: ViewKey) -> Field: ... + def tag(self, graph_key: GraphKey, commitment: Field) -> Field: ... + def get_member(self, name: str) -> Plaintext: ... def serial_number(self, private_key: PrivateKey, program_id: ProgramID, record_identifier: Identifier, record_view_key: Field) -> Field: ... @@ -435,6 +501,20 @@ class Scalar: def from_string(s: str) -> Scalar: ... @staticmethod def zero() -> Scalar: ... + @staticmethod + def one() -> Scalar: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Scalar: ... + def to_bytes_le(self) -> bytes: ... + def to_field(self) -> Field: ... + def add(self, other: Scalar) -> Scalar: ... + def subtract(self, other: Scalar) -> Scalar: ... + def multiply(self, other: Scalar) -> Scalar: ... + def divide(self, other: Scalar) -> Scalar: ... + def __add__(self, other: Scalar) -> Scalar: ... + def __sub__(self, other: Scalar) -> Scalar: ... + def __mul__(self, other: Scalar) -> Scalar: ... + def __truediv__(self, other: Scalar) -> Scalar: ... class Signature: @@ -551,3 +631,5 @@ class ViewKey: def is_owner(self, record_ciphertext: RecordCiphertext) -> bool: ... @property def address(self) -> Address: ... + def to_scalar(self) -> Scalar: ... + def to_field(self) -> Field: ... diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index 5d2af40..b0f8a50 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -20,6 +20,13 @@ class Account: class Address: @staticmethod def from_string(s: str) -> Address: ... + @staticmethod + def from_group(group: Group) -> Address: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Address: ... + def to_group(self) -> Group: ... + def to_field(self) -> Field: ... + def to_bytes_le(self) -> bytes: ... class Authorization: @@ -132,16 +139,63 @@ class Field: def from_u128(u128: int) -> Field: ... @staticmethod def zero() -> Field: ... + @staticmethod + def one() -> Field: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> Field: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Field: ... + def to_bytes_le(self) -> bytes: ... + def double(self) -> Field: ... + def add(self, other: Field) -> Field: ... + def subtract(self, other: Field) -> Field: ... + def multiply(self, other: Field) -> Field: ... + def divide(self, other: Field) -> Field: ... + def pow(self, other: Field) -> Field: ... + def negate(self) -> Field: ... + def to_scalar_lossy(self) -> Scalar: ... + def to_group(self) -> Group: ... + def __add__(self, other: Field) -> Field: ... + def __sub__(self, other: Field) -> Field: ... def __mul__(self, other: Field) -> Field: ... def __truediv__(self, other: Field) -> Field: ... + def __pow__(self, other: Field) -> Field: ... + def __neg__(self) -> Field: ... def to_bits_le(self) -> list[bool]: ... +class GraphKey: + @staticmethod + def from_view_key(view_key: ViewKey) -> GraphKey: ... + @staticmethod + def from_string(s: str) -> GraphKey: ... + @property + def sk_tag(self) -> Field: ... + + class Group: @staticmethod def from_string(s: str) -> Group: ... @staticmethod def zero() -> Group: ... + @staticmethod + def generator() -> Group: ... + @staticmethod + def from_field(field: Field) -> Group: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Group: ... + def to_x_coordinate(self) -> Field: ... + def to_address(self) -> Address: ... + def double(self) -> Group: ... + def to_bytes_le(self) -> bytes: ... + def add(self, other: Group) -> Group: ... + def subtract(self, other: Group) -> Group: ... + def negate(self) -> Group: ... + def scalar_multiply(self, scalar: Scalar) -> Group: ... + def __add__(self, other: Group) -> Group: ... + def __sub__(self, other: Group) -> Group: ... + def __neg__(self) -> Group: ... + def __mul__(self, scalar: Scalar) -> Group: ... class Identifier: @@ -398,6 +452,12 @@ class RecordCiphertext: def from_string(s: str) -> RecordCiphertext: ... def decrypt(self, view_key: ViewKey) -> RecordPlaintext: ... def is_owner(self, view_key: ViewKey) -> bool: ... + @property + def nonce(self) -> Group: ... + def record_view_key(self, view_key: ViewKey) -> Field: ... + def decrypt_with_record_view_key(self, record_view_key: Field) -> RecordPlaintext: ... + @staticmethod + def tag(graph_key: GraphKey, commitment: Field) -> Field: ... class RecordPlaintext: @@ -409,6 +469,12 @@ class RecordPlaintext: def nonce(self) -> Group: ... @property def version(self) -> int: ... + @property + def microcredits(self) -> int: ... + def commitment(self, program_id: ProgramID, record_name: Identifier, record_view_key: Field) -> Field: ... + def record_view_key(self, view_key: ViewKey) -> Field: ... + def tag(self, graph_key: GraphKey, commitment: Field) -> Field: ... + def get_member(self, name: str) -> Plaintext: ... def serial_number(self, private_key: PrivateKey, program_id: ProgramID, record_identifier: Identifier, record_view_key: Field) -> Field: ... @@ -422,6 +488,20 @@ class Scalar: def from_string(s: str) -> Scalar: ... @staticmethod def zero() -> Scalar: ... + @staticmethod + def one() -> Scalar: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Scalar: ... + def to_bytes_le(self) -> bytes: ... + def to_field(self) -> Field: ... + def add(self, other: Scalar) -> Scalar: ... + def subtract(self, other: Scalar) -> Scalar: ... + def multiply(self, other: Scalar) -> Scalar: ... + def divide(self, other: Scalar) -> Scalar: ... + def __add__(self, other: Scalar) -> Scalar: ... + def __sub__(self, other: Scalar) -> Scalar: ... + def __mul__(self, other: Scalar) -> Scalar: ... + def __truediv__(self, other: Scalar) -> Scalar: ... class Signature: @@ -538,3 +618,5 @@ class ViewKey: def is_owner(self, record_ciphertext: RecordCiphertext) -> bool: ... @property def address(self) -> Address: ... + def to_scalar(self) -> Scalar: ... + def to_field(self) -> Field: ... diff --git a/sdk/python/tests/test_records_extra.py b/sdk/python/tests/test_records_extra.py new file mode 100644 index 0000000..0f7b9b8 --- /dev/null +++ b/sdk/python/tests/test_records_extra.py @@ -0,0 +1,409 @@ +# Copyright (C) 2019-2023 Aleo Systems Inc. +# This file is part of the Aleo SDK library. +# Tests for W4a: record-scanning parity (GraphKey, commitments, tags, RVK, arithmetic). +# Source notes: github.com/ProvableHQ/sdk@543b41e wasm/src/... + +import pytest +from aleo.mainnet import ( + Address, + Field, + GraphKey, + Group, + Identifier, + ProgramID, + RecordCiphertext, + RecordPlaintext, + Scalar, + ViewKey, +) +# --------------------------------------------------------------------------- +# KAT constants (vendored from ProvableHQ/sdk@543b41e) +# --------------------------------------------------------------------------- + +# From wasm/src/record/record_ciphertext.rs:223-224 +RECORD_TAG = "1796466189545157638691489609907096471289658804813960182690905095269699169603field" +RECORD_VIEW_KEY = "4445718830394614891114647247073357094867447866913203502139893824059966201724field" + +# Owner record — same ciphertext/VK as existing records.json decrypt_kat +OWNER_CIPHERTEXT = "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q" +OWNER_VIEW_KEY = "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX" +OWNER_PRIVATE_KEY = "APrivateKey1zkpJkyYRGYtkeHDaFfwsKtUJzia7csiWhfBWPXWhXJzy9Ls" + +# V1 credits.aleo record (utilities/test/records.rs:18) +CREDITS_RECORD_V1 = "{ owner: aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a.private, microcredits: 1000000u64.private, _nonce: 3634848344765318974603121890869676775499130077229666060613233255327643175219group.public, _version: 1u8.public }" +CREDITS_RECORD_VIEW_KEY = "5237002936265850807349726649400053591020997883662246784632368923777787639801field" +# Sender ciphertext (noted for future decrypt_sender work, not exposed in this task) +CREDITS_SENDER_CIPHERTEXT = "1182590395568997043375432557467567048762179115999922880321493200728848194550field" +CREDITS_SENDER_PLAINTEXT = "aleo1j92w9mhqznj2hvufad796y8suykjppk7f6n6xmncmktfm95vggzqx4sjlh" + +# Token registry record V1 (utilities/test/records.rs:29-40) +TOKEN_REGISTRY_RECORD_V1 = """{ + owner: aleo1s3ws5tra87fjycnjrwsjcrnw2qxr8jfqqdugnf0xzqqw29q9m5pqem2u4t.private, + amount: 1000u128.private, + token_id: 1751493913335802797273486270793650302076377624243810059080883537084141842600field.private, + external_authorization_required: false.private, + authorized_until: 0u32.private, + _nonce: 353510505137682717871934563523691055502582931368477380633253282125012046603group.public, + _version: 1u8.public + }""" + +# Original credits plaintext (V0 era, 1500000000000000 microcredits) +CREDITS_RECORD_V0 = """{ + owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private, + microcredits: 1500000000000000u64.private, + _nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public, + _version: 0u8.public +}""" + + +# --------------------------------------------------------------------------- +# 1. record_view_key KAT +# --------------------------------------------------------------------------- + +def test_record_view_key_from_ciphertext(): + """RecordCiphertext.record_view_key() matches the known KAT value.""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + rvk = ct.record_view_key(vk) + assert str(rvk) == RECORD_VIEW_KEY + + +def test_record_view_key_from_plaintext(): + """RecordPlaintext.record_view_key() gives the same value.""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + pt = ct.decrypt(vk) + rvk_pt = pt.record_view_key(vk) + assert str(rvk_pt) == RECORD_VIEW_KEY + + +def test_record_view_key_manual_derivation(): + """Manual derivation: (ct.nonce * vk.to_scalar()).to_x_coordinate() == RVK.""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + nonce = ct.nonce + manual_rvk = (nonce * vk.to_scalar()).to_x_coordinate() + assert str(manual_rvk) == RECORD_VIEW_KEY + + +def test_record_view_key_dunder_and_named_agree(): + """Group.__mul__ and scalar_multiply give same result.""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + nonce = ct.nonce + sc = vk.to_scalar() + assert (nonce * sc) == nonce.scalar_multiply(sc) + + +# --------------------------------------------------------------------------- +# 2. decrypt_with_record_view_key +# --------------------------------------------------------------------------- + +def test_decrypt_with_rvk_matches_decrypt(): + """decrypt_with_record_view_key(RVK) produces same plaintext as decrypt(vk).""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + rvk = ct.record_view_key(vk) + pt_via_vk = ct.decrypt(vk) + pt_via_rvk = ct.decrypt_with_record_view_key(rvk) + assert str(pt_via_vk) == str(pt_via_rvk) + + +def test_decrypt_with_wrong_rvk_raises_or_wrong(): + """decrypt_with_record_view_key(Field.one()) yields wrong or raises.""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + pt_correct = ct.decrypt(vk) + wrong_rvk = Field.one() + try: + pt_wrong = ct.decrypt_with_record_view_key(wrong_rvk) + # If it doesn't raise, the owner must not match + assert str(pt_wrong) != str(pt_correct) + except Exception: + pass # raising is also acceptable + + +# --------------------------------------------------------------------------- +# 3. commitment + tag KAT +# --------------------------------------------------------------------------- + +def test_commitment_and_tag_kat(): + """Full commitment->tag pipeline matches RECORD_TAG.""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + gk = GraphKey.from_view_key(vk) + pt = ct.decrypt(vk) + rvk = ct.record_view_key(vk) + + prog = ProgramID.from_string("credits.aleo") + rec_name = Identifier.from_string("credits") + commitment = pt.commitment(prog, rec_name, rvk) + tag = pt.tag(gk, commitment) + assert str(tag) == RECORD_TAG + + +def test_tag_static_method_matches(): + """RecordCiphertext.tag (static) gives same result as RecordPlaintext.tag.""" + ct = RecordCiphertext.from_string(OWNER_CIPHERTEXT) + vk = ViewKey.from_string(OWNER_VIEW_KEY) + gk = GraphKey.from_view_key(vk) + pt = ct.decrypt(vk) + rvk = ct.record_view_key(vk) + + prog = ProgramID.from_string("credits.aleo") + rec_name = Identifier.from_string("credits") + commitment = pt.commitment(prog, rec_name, rvk) + + tag_from_pt = pt.tag(gk, commitment) + tag_from_ct = RecordCiphertext.tag(gk, commitment) + assert tag_from_pt == tag_from_ct + assert str(tag_from_pt) == RECORD_TAG + + +# --------------------------------------------------------------------------- +# 4. GraphKey +# --------------------------------------------------------------------------- + +def test_graph_key_from_view_key_deterministic(): + """GraphKey.from_view_key is deterministic.""" + vk = ViewKey.from_string(OWNER_VIEW_KEY) + gk1 = GraphKey.from_view_key(vk) + gk2 = GraphKey.from_view_key(vk) + assert gk1 == gk2 + + +def test_graph_key_sk_tag_is_field(): + """GraphKey.sk_tag returns a Field.""" + vk = ViewKey.from_string(OWNER_VIEW_KEY) + gk = GraphKey.from_view_key(vk) + assert isinstance(gk.sk_tag, Field) + + +def test_graph_key_roundtrip(): + """GraphKey.from_string(str(gk)) round-trips.""" + vk = ViewKey.from_string(OWNER_VIEW_KEY) + gk = GraphKey.from_view_key(vk) + gk2 = GraphKey.from_string(str(gk)) + assert gk == gk2 + + +# --------------------------------------------------------------------------- +# 5. get_member + microcredits +# --------------------------------------------------------------------------- + +def test_credits_record_v0_microcredits(): + """V0 credits record microcredits == 1500000000000000.""" + pt = RecordPlaintext.from_string(CREDITS_RECORD_V0) + assert pt.microcredits == 1500000000000000 + + +def test_token_registry_microcredits_zero(): + """Token registry record has no microcredits field, returns 0.""" + pt = RecordPlaintext.from_string(TOKEN_REGISTRY_RECORD_V1) + assert pt.microcredits == 0 + + +def test_token_registry_get_member_amount(): + """get_member('amount') on token registry record returns a Plaintext with 1000u128.""" + pt = RecordPlaintext.from_string(TOKEN_REGISTRY_RECORD_V1) + member = pt.get_member("amount") + assert "1000u128" in str(member) + + +def test_get_member_nonexistent_raises(): + """get_member on a missing key raises an exception.""" + pt = RecordPlaintext.from_string(CREDITS_RECORD_V0) + with pytest.raises(Exception): + pt.get_member("nonexistent_field") + + +# --------------------------------------------------------------------------- +# 6. Field arithmetic sanity checks +# --------------------------------------------------------------------------- + +def test_field_add(): + a = Field.from_string("2field") + b = Field.from_string("3field") + assert str(a + b) == "5field" + assert str(a.add(b)) == "5field" + assert (a + b) == a.add(b) + + +def test_field_sub(): + a = Field.from_string("5field") + b = Field.from_string("3field") + assert str(a - b) == "2field" + assert str(a.subtract(b)) == "2field" + assert (a - b) == a.subtract(b) + + +def test_field_neg(): + a = Field.from_string("1field") + neg_a = -a + assert str(neg_a.add(a)) == str(Field.zero()) + assert neg_a == a.negate() + + +def test_field_pow(): + two = Field.from_string("2field") + three = Field.from_string("3field") + result = two ** three + assert str(result) == "8field" + assert result == two.pow(three) + + +def test_field_double(): + a = Field.from_string("5field") + assert str(a.double()) == "10field" + + +def test_field_one(): + one = Field.one() + assert str(one) == "1field" + + +def test_field_add_sub_roundtrip(): + a = Field.from_string("12345field") + b = Field.from_string("67890field") + assert (a + b) - b == a + + +def test_field_bytes_roundtrip(): + a = Field.from_string("42field") + b = Field.from_bytes_le(a.to_bytes_le()) + assert a == b + + +def test_field_dunder_mul_matches_multiply(): + a = Field.from_string("7field") + b = Field.from_string("6field") + assert (a * b) == a.multiply(b) + + +# --------------------------------------------------------------------------- +# 7. Scalar arithmetic sanity checks +# --------------------------------------------------------------------------- + +def test_scalar_add(): + a = Scalar.from_string("2scalar") + b = Scalar.from_string("3scalar") + assert str(a + b) == "5scalar" + assert (a + b) == a.add(b) + + +def test_scalar_mul(): + a = Scalar.from_string("4scalar") + b = Scalar.from_string("5scalar") + assert str(a * b) == "20scalar" + assert (a * b) == a.multiply(b) + + +def test_scalar_div(): + a = Scalar.from_string("6scalar") + b = Scalar.from_string("3scalar") + # a / b * b == a + assert (a / b) * b == a + assert (a / b) == a.divide(b) + + +def test_scalar_div_zero(): + a = Scalar.from_string("1scalar") + with pytest.raises(ZeroDivisionError): + _ = a / Scalar.zero() + + +def test_scalar_one(): + one = Scalar.one() + assert str(one) == "1scalar" + + +def test_scalar_bytes_roundtrip(): + a = Scalar.from_string("99scalar") + b = Scalar.from_bytes_le(a.to_bytes_le()) + assert a == b + + +def test_scalar_to_field(): + one = Scalar.one() + f = one.to_field() + assert isinstance(f, Field) + # 1scalar -> 1field + assert str(f) == "1field" + + +# --------------------------------------------------------------------------- +# 8. Group arithmetic sanity checks +# --------------------------------------------------------------------------- + +def test_group_generator_double(): + g = Group.generator() + assert g.double() == g + g + assert g.double() == g.add(g) + + +def test_group_add_sub(): + g = Group.generator() + h = g.double() + assert h - g == g + assert h.subtract(g) == g + assert (h - g) == h.subtract(g) + + +def test_group_neg(): + g = Group.generator() + assert g + (-g) == Group.zero() + assert g.add(g.negate()) == Group.zero() + assert (-g) == g.negate() + + +def test_group_scalar_multiply(): + g = Group.generator() + two = Scalar.from_string("2scalar") + assert g * two == g.double() + assert g.scalar_multiply(two) == g.double() + assert (g * two) == g.scalar_multiply(two) + + +def test_group_to_x_coordinate(): + g = Group.generator() + x = g.to_x_coordinate() + assert isinstance(x, Field) + + +def test_group_to_address_roundtrip(): + vk = ViewKey.from_string(OWNER_VIEW_KEY) + addr = vk.address + group = addr.to_group() + addr2 = group.to_address() + assert str(addr) == str(addr2) + + +def test_address_to_group_to_address(): + """Address.to_group().to_address() round-trips.""" + vk = ViewKey.from_string(OWNER_VIEW_KEY) + addr = vk.address + assert addr == Address.from_group(addr.to_group()) + + +def test_field_bytes_le_roundtrip_group(): + """Group bytes LE round-trip.""" + g = Group.generator() + b = g.to_bytes_le() + g2 = Group.from_bytes_le(b) + assert g == g2 + + +# --------------------------------------------------------------------------- +# 9. ViewKey conversions +# --------------------------------------------------------------------------- + +def test_view_key_to_scalar(): + vk = ViewKey.from_string(OWNER_VIEW_KEY) + sc = vk.to_scalar() + assert isinstance(sc, Scalar) + + +def test_view_key_to_field(): + vk = ViewKey.from_string(OWNER_VIEW_KEY) + f = vk.to_field() + assert isinstance(f, Field) diff --git a/sdk/src/account/address.rs b/sdk/src/account/address.rs index 183a11a..e130592 100644 --- a/sdk/src/account/address.rs +++ b/sdk/src/account/address.rs @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::types::AddressNative; +use crate::{types::AddressNative, Field, Group}; use pyo3::prelude::*; +use snarkvm::prelude::ToField; use std::{ collections::hash_map::DefaultHasher, @@ -30,6 +31,13 @@ use std::{ #[derive(Clone)] pub struct Address(AddressNative); +impl Address { + /// Initializes an address from a group element (for internal use). + pub(crate) fn new(group: crate::types::GroupNative) -> Self { + Self(AddressNative::new(group)) + } +} + #[pymethods] impl Address { /// Reads in an account address string. @@ -38,6 +46,36 @@ impl Address { AddressNative::from_str(s).map(Self) } + /// Returns the address as a group element. + fn to_group(&self) -> Group { + (*self.0.to_group()).into() + } + + /// Returns the address as a field element (x-coordinate). + fn to_field(&self) -> anyhow::Result<Field> { + self.0.to_field().map(Into::into) + } + + /// Returns the little-endian byte representation of the address. + fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { + use snarkvm::prelude::ToBytes; + self.0.to_bytes_le() + } + + /// Parses an address from little-endian bytes. + #[staticmethod] + fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { + use snarkvm::prelude::FromBytes; + AddressNative::from_bytes_le(&bytes).map(Self) + } + + /// Recovers an address from a group element. + #[staticmethod] + fn from_group(group: &Group) -> anyhow::Result<Self> { + // Address is built on top of Group, so just wrap the inner group element directly. + Ok(Self(AddressNative::new(**group))) + } + /// Returns the address as a base58 string. fn __str__(&self) -> String { self.0.to_string() diff --git a/sdk/src/account/graph_key.rs b/sdk/src/account/graph_key.rs new file mode 100644 index 0000000..6334561 --- /dev/null +++ b/sdk/src/account/graph_key.rs @@ -0,0 +1,87 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{types::GraphKeyNative, Field, ViewKey}; + +use pyo3::prelude::*; + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + ops::Deref, + str::FromStr, +}; + +/// The account graph key used for record scanning. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct GraphKey(GraphKeyNative); + +#[pymethods] +impl GraphKey { + /// Derives the graph key from the given view key. + #[staticmethod] + fn from_view_key(view_key: &ViewKey) -> anyhow::Result<Self> { + GraphKeyNative::try_from(&**view_key).map(Self) + } + + /// Reads in an account graph key from a base58 string. + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + GraphKeyNative::from_str(s).map(Self) + } + + /// Returns the graph key tag `sk_tag`. + #[getter] + fn sk_tag(&self) -> Field { + self.0.sk_tag().into() + } + + /// Returns the graph key as a base58 string. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } + + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } +} + +impl Deref for GraphKey { + type Target = GraphKeyNative; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<GraphKeyNative> for GraphKey { + fn from(value: GraphKeyNative) -> Self { + Self(value) + } +} + +impl From<GraphKey> for GraphKeyNative { + fn from(value: GraphKey) -> Self { + value.0 + } +} diff --git a/sdk/src/account/mod.rs b/sdk/src/account/mod.rs index 39d3a12..4392474 100644 --- a/sdk/src/account/mod.rs +++ b/sdk/src/account/mod.rs @@ -20,6 +20,9 @@ pub use address::Address; mod compute_key; pub use compute_key::ComputeKey; +mod graph_key; +pub use graph_key::GraphKey; + mod private_key; pub use private_key::PrivateKey; diff --git a/sdk/src/account/record.rs b/sdk/src/account/record.rs index 074a524..b9af8a2 100644 --- a/sdk/src/account/record.rs +++ b/sdk/src/account/record.rs @@ -15,15 +15,18 @@ // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. use crate::{ - types::{RecordCiphertextNative, RecordPlaintextNative}, - Field, Group, Identifier, PrivateKey, ProgramID, ViewKey, + types::{ + IdentifierNative, LiteralNative, PlaintextNative, RecordCiphertextNative, + RecordPlaintextNative, + }, + Field, GraphKey, Group, Identifier, Plaintext, PrivateKey, ProgramID, ViewKey, }; +use snarkvm::prelude::Entry; use std::ops::Deref; +use std::str::FromStr; use pyo3::prelude::*; -use std::str::FromStr; - /// A value(ciphertext) stored in program record. #[pyclass(frozen)] pub struct RecordCiphertext(RecordCiphertextNative); @@ -46,6 +49,35 @@ impl RecordCiphertext { self.0.is_owner(view_key) } + /// Returns the nonce of the record ciphertext. + #[getter] + fn nonce(&self) -> Group { + (*self.0.nonce()).into() + } + + /// Computes the record view key as `(nonce * view_key_scalar).to_x_coordinate()`. + pub fn record_view_key(&self, view_key: &ViewKey) -> Field { + (*self.0.nonce() * ***view_key).to_x_coordinate().into() + } + + /// Decrypts self into plaintext using the given record view key (unchecked — no owner verification). + pub fn decrypt_with_record_view_key( + &self, + record_view_key: &Field, + ) -> anyhow::Result<RecordPlaintext> { + self.0 + .decrypt_symmetric_unchecked(&**record_view_key) + .map(Into::into) + } + + /// Computes the record tag from a graph key and commitment. + #[staticmethod] + fn tag(graph_key: &GraphKey, commitment: &Field) -> anyhow::Result<Field> { + #[allow(clippy::useless_conversion)] + RecordPlaintextNative::tag(graph_key.sk_tag().into(), commitment.clone().into()) + .map(Into::into) + } + /// Returns the record ciphertext as a string. fn __str__(&self) -> String { self.0.to_string() @@ -85,7 +117,7 @@ impl RecordPlaintext { **self.0.version() } - /// Returns the owner of the record as a string + /// Returns the owner of the record as a string. #[getter] fn owner(&self) -> String { self.0.owner().to_string() @@ -97,6 +129,64 @@ impl RecordPlaintext { (*self.0.nonce()).into() } + /// Returns the amount of microcredits in the record (0 if no microcredits field). + #[getter] + fn microcredits(&self) -> u64 { + self.0 + .find(&[IdentifierNative::from_str("microcredits").unwrap()]) + .ok() + .and_then(|entry| match entry { + Entry::Private(PlaintextNative::Literal(LiteralNative::U64(amount), _)) => { + Some(*amount) + } + Entry::Public(PlaintextNative::Literal(LiteralNative::U64(amount), _)) => { + Some(*amount) + } + _ => None, + }) + .unwrap_or(0) + } + + /// Returns the commitment of the record for the given program_id, record_name, and record_view_key. + pub fn commitment( + &self, + program_id: &ProgramID, + record_name: &Identifier, + record_view_key: &Field, + ) -> anyhow::Result<Field> { + self.0 + .to_commitment(&**program_id, &**record_name, &**record_view_key) + .map(Into::into) + } + + /// Computes the record view key as `(nonce * view_key_scalar).to_x_coordinate()`. + pub fn record_view_key(&self, view_key: &ViewKey) -> Field { + (*self.0.nonce() * ***view_key).to_x_coordinate().into() + } + + /// Computes the record tag from a graph key and commitment. + pub fn tag(&self, graph_key: &GraphKey, commitment: &Field) -> anyhow::Result<Field> { + #[allow(clippy::useless_conversion)] + RecordPlaintextNative::tag(graph_key.sk_tag().into(), commitment.clone().into()) + .map(Into::into) + } + + /// Returns the record entry with the given name as a Plaintext. + pub fn get_member(&self, name: &str) -> anyhow::Result<Plaintext> { + let id = IdentifierNative::from_str(name)?; + let entry = self + .0 + .data() + .get(&id) + .ok_or_else(|| anyhow::anyhow!("Record member '{}' not found", name))?; + let plaintext = match entry { + Entry::Constant(p) => p.clone(), + Entry::Public(p) => p.clone(), + Entry::Private(p) => p.clone(), + }; + Ok(plaintext.into()) + } + /// Attempt to get the serial number of a record to determine whether or not is has been spent pub fn serial_number( &self, @@ -105,7 +195,9 @@ impl RecordPlaintext { record_identifier: &Identifier, record_view_key: &Field, ) -> anyhow::Result<Field> { - let commitment = self.to_commitment(program_id, record_identifier, &**record_view_key)?; + let commitment = + self.0 + .to_commitment(&**program_id, &**record_identifier, &**record_view_key)?; RecordPlaintextNative::serial_number(**private_key, commitment).map(Into::into) } diff --git a/sdk/src/account/view_key.rs b/sdk/src/account/view_key.rs index 581280f..b08ee74 100644 --- a/sdk/src/account/view_key.rs +++ b/sdk/src/account/view_key.rs @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::{types::ViewKeyNative, Address, RecordCiphertext, RecordPlaintext}; +use crate::{types::ViewKeyNative, Address, Field, RecordCiphertext, RecordPlaintext, Scalar}; use pyo3::prelude::*; +use snarkvm::prelude::ToField; use std::{ collections::hash_map::DefaultHasher, @@ -54,6 +55,16 @@ impl ViewKey { self.0.to_address().into() } + /// Returns the view key as a scalar. + pub fn to_scalar(&self) -> Scalar { + (*self.0).into() + } + + /// Returns the view key as a field element. + pub fn to_field(&self) -> anyhow::Result<Field> { + self.0.to_field().map(Into::into) + } + /// Returns the view key as a base58 string. fn __str__(&self) -> String { self.0.to_string() diff --git a/sdk/src/algebra/field.rs b/sdk/src/algebra/field.rs index 8500e97..318c9bd 100644 --- a/sdk/src/algebra/field.rs +++ b/sdk/src/algebra/field.rs @@ -14,11 +14,11 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::types::FieldNative; +use crate::{types::FieldNative, Group, Scalar}; use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; use rand::rngs::StdRng; -use snarkvm::prelude::{ToBits, Uniform, Zero}; +use snarkvm::prelude::{Double, FromBits, One, Pow, ToBits, Uniform, Zero}; use std::{ collections::hash_map::DefaultHasher, @@ -64,16 +64,53 @@ impl Field { FieldNative::zero().into() } - /// Returns the Field as a string. - fn __str__(&self) -> String { - self.0.to_string() + /// Returns the `1` element of the field. + #[staticmethod] + fn one() -> Self { + FieldNative::one().into() } - fn __mul__(&self, other: Self) -> Self { + /// Constructs a field from little-endian bits. + #[staticmethod] + fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + FieldNative::from_bits_le(&bits).map(Self) + } + + /// Returns the little-endian byte representation of the field element. + fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { + use snarkvm::prelude::ToBytes; + self.0.to_bytes_le() + } + + /// Parses a field from little-endian bytes. + #[staticmethod] + fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { + use snarkvm::prelude::FromBytes; + FieldNative::from_bytes_le(&bytes).map(Self) + } + + /// Returns the double of this field element. + fn double(&self) -> Self { + Self(Double::double(&self.0)) + } + + /// Returns the sum of self and other. + fn add(&self, other: Self) -> Self { + Self(self.0 + other.0) + } + + /// Returns the difference of self and other. + fn subtract(&self, other: Self) -> Self { + Self(self.0 - other.0) + } + + /// Returns the product of self and other. + fn multiply(&self, other: Self) -> Self { Self(self.0 * other.0) } - fn __truediv__(&self, other: Self) -> PyResult<Self> { + /// Returns self divided by other (panics on zero divisor). + fn divide(&self, other: Self) -> PyResult<Self> { if other.is_zero() { Err(PyZeroDivisionError::new_err("division by zero")) } else { @@ -81,6 +118,42 @@ impl Field { } } + /// Returns self raised to the power of other. + fn pow(&self, other: Self) -> Self { + Self(Pow::pow(self.0, other.0)) + } + + /// Returns the negation of self. + fn negate(&self) -> Self { + Self(-self.0) + } + + /// Converts the field to a scalar using lossy truncation. + fn to_scalar_lossy(&self) -> Scalar { + use crate::types::ScalarNative; + ScalarNative::from_field_lossy(&self.0).into() + } + + /// Converts the field to a group element (x-coordinate recovery). + fn to_group(&self) -> anyhow::Result<Group> { + use crate::types::GroupNative; + use snarkvm::prelude::FromField; + GroupNative::from_field(&self.0).map(Group::from) + } + + /// Returns the Field as a string. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __mul__(&self, other: Self) -> Self { + self.multiply(other) + } + + fn __truediv__(&self, other: Self) -> PyResult<Self> { + self.divide(other) + } + fn __eq__(&self, other: &Self) -> bool { self.0 == other.0 } @@ -91,6 +164,22 @@ impl Field { hasher.finish() } + fn __add__(&self, other: Self) -> Self { + self.add(other) + } + + fn __sub__(&self, other: Self) -> Self { + self.subtract(other) + } + + fn __neg__(&self) -> Self { + self.negate() + } + + fn __pow__(&self, other: Self, _modulo: Option<Self>) -> Self { + self.pow(other) + } + /// Returns the little-endian bit representation of the field element. pub fn to_bits_le(&self) -> Vec<bool> { self.0.to_bits_le() diff --git a/sdk/src/algebra/group.rs b/sdk/src/algebra/group.rs index a09661d..0fe17d5 100644 --- a/sdk/src/algebra/group.rs +++ b/sdk/src/algebra/group.rs @@ -14,10 +14,10 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::types::GroupNative; +use crate::{types::GroupNative, Address, Field, Scalar}; use pyo3::prelude::*; -use snarkvm::prelude::Zero; +use snarkvm::prelude::{Double, FromField, Zero}; use std::{ collections::hash_map::DefaultHasher, @@ -45,6 +45,68 @@ impl Group { GroupNative::zero().into() } + /// Returns the prime subgroup generator. + #[staticmethod] + fn generator() -> Self { + GroupNative::generator().into() + } + + /// Returns the group from a field element (x-coordinate recovery). + #[staticmethod] + fn from_field(field: &Field) -> anyhow::Result<Self> { + GroupNative::from_field(&**field).map(Self) + } + + /// Returns the x-coordinate of the group element as a field. + fn to_x_coordinate(&self) -> Field { + self.0.to_x_coordinate().into() + } + + /// Returns the address corresponding to this group element. + fn to_address(&self) -> Address { + Address::new(self.0) + } + + /// Returns the double of this group element. + fn double(&self) -> Self { + Self(Double::double(&self.0)) + } + + /// Returns the sum of self and other. + fn add(&self, other: &Self) -> Self { + Self(self.0 + other.0) + } + + /// Returns the difference of self and other. + fn subtract(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + + /// Returns the negation of self. + fn negate(&self) -> Self { + Self(-self.0) + } + + /// Returns self scaled by scalar (scalar multiplication). + fn scalar_multiply(&self, scalar: &Scalar) -> Self { + Self(self.0 * **scalar) + } + + /// Returns the little-endian byte representation of the group element (x-coordinate). + fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { + use snarkvm::prelude::ToBytes; + self.0.to_bytes_le() + } + + /// Parses a group from little-endian bytes (x-coordinate). + #[staticmethod] + fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { + use snarkvm::prelude::FromBytes; + GroupNative::from_bytes_le(&bytes).map(Self) + } + + // ---------- dunders ---------- + /// Returns the Group as a string. fn __str__(&self) -> String { self.0.to_string() @@ -59,6 +121,22 @@ impl Group { self.0.hash(&mut hasher); hasher.finish() } + + fn __add__(&self, other: &Self) -> Self { + self.add(other) + } + + fn __sub__(&self, other: &Self) -> Self { + self.subtract(other) + } + + fn __neg__(&self) -> Self { + self.negate() + } + + fn __mul__(&self, scalar: &Scalar) -> Self { + self.scalar_multiply(scalar) + } } impl Deref for Group { @@ -80,3 +158,9 @@ impl From<Group> for GroupNative { value.0 } } + +impl From<&GroupNative> for Group { + fn from(value: &GroupNative) -> Self { + Self(*value) + } +} diff --git a/sdk/src/algebra/scalar.rs b/sdk/src/algebra/scalar.rs index 7b3ba43..cc1c421 100644 --- a/sdk/src/algebra/scalar.rs +++ b/sdk/src/algebra/scalar.rs @@ -14,10 +14,10 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::types::ScalarNative; +use crate::{types::ScalarNative, Field}; -use pyo3::prelude::*; -use snarkvm::prelude::Zero; +use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; +use snarkvm::prelude::{One, ToField, Zero}; use std::{ collections::hash_map::DefaultHasher, @@ -45,6 +45,54 @@ impl Scalar { ScalarNative::zero().into() } + /// Returns the `1` element of the scalar. + #[staticmethod] + fn one() -> Self { + ScalarNative::one().into() + } + + /// Returns the little-endian byte representation of the scalar element. + fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { + use snarkvm::prelude::ToBytes; + self.0.to_bytes_le() + } + + /// Parses a scalar from little-endian bytes. + #[staticmethod] + fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { + use snarkvm::prelude::FromBytes; + ScalarNative::from_bytes_le(&bytes).map(Self) + } + + /// Returns this scalar as a field element. + fn to_field(&self) -> anyhow::Result<Field> { + self.0.to_field().map(Into::into) + } + + /// Returns the sum of self and other. + fn add(&self, other: Self) -> Self { + Self(self.0 + other.0) + } + + /// Returns the difference of self and other. + fn subtract(&self, other: Self) -> Self { + Self(self.0 - other.0) + } + + /// Returns the product of self and other. + fn multiply(&self, other: Self) -> Self { + Self(self.0 * other.0) + } + + /// Returns self divided by other (panics on zero divisor). + fn divide(&self, other: Self) -> PyResult<Self> { + if other.is_zero() { + Err(PyZeroDivisionError::new_err("division by zero")) + } else { + Ok(Self(self.0 / other.0)) + } + } + /// Returns the Scalar as a string. fn __str__(&self) -> String { self.0.to_string() @@ -59,6 +107,22 @@ impl Scalar { self.0.hash(&mut hasher); hasher.finish() } + + fn __add__(&self, other: Self) -> Self { + self.add(other) + } + + fn __sub__(&self, other: Self) -> Self { + self.subtract(other) + } + + fn __mul__(&self, other: Self) -> Self { + self.multiply(other) + } + + fn __truediv__(&self, other: Self) -> PyResult<Self> { + self.divide(other) + } } impl Deref for Scalar { diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index d7d66f9..38568ea 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -53,6 +53,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Execution>()?; m.add_class::<Fee>()?; m.add_class::<Field>()?; + m.add_class::<GraphKey>()?; m.add_class::<Group>()?; m.add_class::<Identifier>()?; m.add_class::<I8>()?; diff --git a/sdk/src/types.rs b/sdk/src/types.rs index 5b39a90..0ceebd9 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -26,10 +26,10 @@ use snarkvm::ledger::block::{Fee, Transaction}; use snarkvm::ledger::query::Query; use snarkvm::ledger::store::helpers::memory::BlockMemory; use snarkvm::prelude::{ - Address, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, Group, Identifier, - Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, ProvingKey, Record, Response, - Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, I128, I16, I32, I64, I8, U128, - U16, U32, U64, U8, + Address, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, GraphKey, Group, + Identifier, Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, ProvingKey, Record, + Response, Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, I128, I16, I32, I64, I8, + U128, U16, U32, U64, U8, }; use snarkvm::synthesizer::{Process, Trace}; @@ -46,6 +46,7 @@ pub type CurrentAleo = AleoTestnetV0; // Account types pub type AddressNative = Address<CurrentNetwork>; pub type ComputeKeyNative = ComputeKey<CurrentNetwork>; +pub type GraphKeyNative = GraphKey<CurrentNetwork>; pub type PrivateKeyNative = PrivateKey<CurrentNetwork>; pub type SignatureNative = Signature<CurrentNetwork>; pub type ViewKeyNative = ViewKey<CurrentNetwork>; From 7e1c9608ee2662e3e5bae623580d64503a2c7521 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 07:12:46 -0400 Subject: [PATCH 36/72] =?UTF-8?q?fix:=20complete=20Scalar=20convention=20?= =?UTF-8?q?=E2=80=94=20pow/negate=20twins=20+=20equivalence=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 4 ++++ sdk/python/aleo/_aleolib_mainnet.pyi | 4 ++++ sdk/python/tests/test_records_extra.py | 17 +++++++++++++++++ sdk/src/algebra/scalar.rs | 20 +++++++++++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index 6c24087..2ba9cbd 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -511,6 +511,10 @@ class Scalar: def subtract(self, other: Scalar) -> Scalar: ... def multiply(self, other: Scalar) -> Scalar: ... def divide(self, other: Scalar) -> Scalar: ... + def pow(self, other: Scalar) -> Scalar: ... + def negate(self) -> Scalar: ... + def __pow__(self, other: Scalar) -> Scalar: ... + def __neg__(self) -> Scalar: ... def __add__(self, other: Scalar) -> Scalar: ... def __sub__(self, other: Scalar) -> Scalar: ... def __mul__(self, other: Scalar) -> Scalar: ... diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index b0f8a50..54bd7d7 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -498,6 +498,10 @@ class Scalar: def subtract(self, other: Scalar) -> Scalar: ... def multiply(self, other: Scalar) -> Scalar: ... def divide(self, other: Scalar) -> Scalar: ... + def pow(self, other: Scalar) -> Scalar: ... + def negate(self) -> Scalar: ... + def __pow__(self, other: Scalar) -> Scalar: ... + def __neg__(self) -> Scalar: ... def __add__(self, other: Scalar) -> Scalar: ... def __sub__(self, other: Scalar) -> Scalar: ... def __mul__(self, other: Scalar) -> Scalar: ... diff --git a/sdk/python/tests/test_records_extra.py b/sdk/python/tests/test_records_extra.py index 0f7b9b8..9efbce6 100644 --- a/sdk/python/tests/test_records_extra.py +++ b/sdk/python/tests/test_records_extra.py @@ -306,6 +306,23 @@ def test_scalar_div(): assert (a / b) == a.divide(b) +def test_scalar_sub(): + a = Scalar.from_string("5scalar") + b = Scalar.from_string("3scalar") + assert str(a - b) == "2scalar" + assert (a - b) == a.subtract(b) + + +def test_scalar_pow_and_negate(): + a = Scalar.from_string("2scalar") + b = Scalar.from_string("3scalar") + assert str(a**b) == "8scalar" + assert (a**b) == a.pow(b) + # negation: a + (-a) == 0 + assert (a + (-a)) == Scalar.zero() + assert (-a) == a.negate() + + def test_scalar_div_zero(): a = Scalar.from_string("1scalar") with pytest.raises(ZeroDivisionError): diff --git a/sdk/src/algebra/scalar.rs b/sdk/src/algebra/scalar.rs index cc1c421..de02990 100644 --- a/sdk/src/algebra/scalar.rs +++ b/sdk/src/algebra/scalar.rs @@ -17,7 +17,7 @@ use crate::{types::ScalarNative, Field}; use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; -use snarkvm::prelude::{One, ToField, Zero}; +use snarkvm::prelude::{One, Pow, ToField, Zero}; use std::{ collections::hash_map::DefaultHasher, @@ -93,6 +93,16 @@ impl Scalar { } } + /// Returns self raised to the power of other. + fn pow(&self, other: Self) -> Self { + Self(Pow::pow(self.0, other.0)) + } + + /// Returns the negation of self. + fn negate(&self) -> Self { + Self(-self.0) + } + /// Returns the Scalar as a string. fn __str__(&self) -> String { self.0.to_string() @@ -123,6 +133,14 @@ impl Scalar { fn __truediv__(&self, other: Self) -> PyResult<Self> { self.divide(other) } + + fn __pow__(&self, other: Self, _modulo: Option<u32>) -> Self { + self.pow(other) + } + + fn __neg__(&self) -> Self { + self.negate() + } } impl Deref for Scalar { From e0b5ef126c6ccbb3d292d86412400596ca94b80c Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 07:46:12 -0400 Subject: [PATCH 37/72] feat: full wasm parity for account structs, Value, and Plaintext Implements W4b: full method parity with ProvableHQ/sdk wasm @543b41e. New type: - PrivateKeyCiphertext: wraps CiphertextNative with inline Encryptor scheme (hash_psd2 + encrypt_symmetric), interoperates with Python encryptor.py Address additions (19 methods): - from_program_id, is_valid, to/from_bits_le, to/from_fields, to_plaintext, to_scalar_lossy, to_boolean_lossy, 10 integer lossy casts PrivateKey additions (7 methods): - from_seed_unchecked, sign_value, bytes/from_bytes, new_encrypted, to_ciphertext, from_private_key_ciphertext ViewKey: bytes / from_bytes pair Signature additions (9 methods): - to_address, to_fields, to_bits_le, bytes/from_bytes/from_bits_le, to_plaintext, sign_value, verify_value Plaintext additions (13 methods): - find, bytes/from_bytes, to_bits_le, to_bits_raw_le/be, to_bytes_raw_le/be, to/from_fields, to_fields_raw, plaintext_type getter, to_python Value additions (11 methods): - from_plaintext, to_plaintext, to_record_plaintext, is_plaintext/is_record/is_future, value_type getter, bytes/from_bytes, to_bits_le, to_fields Infrastructure: - From<NativeType> impls added to all integer and boolean algebra types - PrivateKeyCiphertext registered in lib.rs between Plaintext and Poseidon2 - All new methods stubbed in _aleolib_mainnet.pyi Tests: 241 pass (176 pre-existing + 65 new KAT + parity tests) Cargo fmt + clippy -D warnings clean; testnet cfg compiles Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/_aleolib_mainnet.pyi | 134 +++++++-- sdk/python/tests/test_account_parity.py | 316 ++++++++++++++++++++++ sdk/python/tests/test_value_plaintext.py | 233 ++++++++++++++++ sdk/src/account/address.rs | 120 +++++++- sdk/src/account/mod.rs | 3 + sdk/src/account/private_key.rs | 68 ++++- sdk/src/account/private_key_ciphertext.rs | 145 ++++++++++ sdk/src/account/signature.rs | 63 ++++- sdk/src/account/text.rs | 142 +++++++++- sdk/src/account/view_key.rs | 13 +- sdk/src/algebra/boolean.rs | 6 + sdk/src/algebra/integer.rs | 6 + sdk/src/lib.rs | 1 + sdk/src/programs/value.rs | 77 +++++- 14 files changed, 1295 insertions(+), 32 deletions(-) create mode 100644 sdk/python/tests/test_account_parity.py create mode 100644 sdk/python/tests/test_value_plaintext.py create mode 100644 sdk/src/account/private_key_ciphertext.rs diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index 54bd7d7..69d46c1 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -19,14 +19,37 @@ class Account: class Address: @staticmethod - def from_string(s: str) -> Address: ... + def from_string(s: str) -> "Address": ... @staticmethod - def from_group(group: Group) -> Address: ... + def from_group(group: "Group") -> "Address": ... @staticmethod - def from_bytes_le(bytes: bytes) -> Address: ... - def to_group(self) -> Group: ... - def to_field(self) -> Field: ... + def from_bytes_le(bytes: bytes) -> "Address": ... + @staticmethod + def from_program_id(program_id: str) -> "Address": ... + @staticmethod + def is_valid(address: str) -> bool: ... + def to_group(self) -> "Group": ... + def to_field(self) -> "Field": ... def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> "Address": ... + def to_fields(self) -> list["Field"]: ... + @staticmethod + def from_fields(fields: list["Field"]) -> "Address": ... + def to_plaintext(self) -> "Plaintext": ... + def to_scalar_lossy(self) -> "Scalar": ... + def to_boolean_lossy(self) -> "Boolean": ... + def to_u8_lossy(self) -> "U8": ... + def to_u16_lossy(self) -> "U16": ... + def to_u32_lossy(self) -> "U32": ... + def to_u64_lossy(self) -> "U64": ... + def to_u128_lossy(self) -> "U128": ... + def to_i8_lossy(self) -> "I8": ... + def to_i16_lossy(self) -> "I16": ... + def to_i32_lossy(self) -> "I32": ... + def to_i64_lossy(self) -> "I64": ... + def to_i128_lossy(self) -> "I128": ... class Authorization: @@ -336,9 +359,36 @@ class Plaintext: def is_literal(self) -> bool: ... def is_struct(self) -> bool: ... def is_array(self) -> bool: ... - def as_literal(self) -> Literal: ... - def as_struct(self) -> dict[Identifier, Plaintext]: ... - def as_array(self) -> list[Plaintext]: ... + def as_literal(self) -> "Literal": ... + def as_struct(self) -> dict["Identifier", "Plaintext"]: ... + def as_array(self) -> list["Plaintext"]: ... + def find(self, path: list[str]) -> "Plaintext": ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> "Plaintext": ... + def to_bits_le(self) -> list[bool]: ... + def to_bits_raw_le(self) -> list[bool]: ... + def to_bits_raw_be(self) -> list[bool]: ... + def to_bytes_raw_le(self) -> bytes: ... + def to_bytes_raw_be(self) -> bytes: ... + def to_fields(self) -> list["Field"]: ... + @staticmethod + def from_fields(fields: list["Field"]) -> "Plaintext": ... + def to_fields_raw(self) -> list["Field"]: ... + @property + def plaintext_type(self) -> str: ... + def to_python(self) -> object: ... + + +class PrivateKeyCiphertext: + @staticmethod + def encrypt_private_key(private_key: "PrivateKey", secret: str) -> "PrivateKeyCiphertext": ... + def decrypt_to_private_key(self, secret: str) -> "PrivateKey": ... + @staticmethod + def from_string(s: str) -> "PrivateKeyCiphertext": ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... class Poseidon2: @@ -386,9 +436,20 @@ class PrivateKey: def compute_key(self) -> ComputeKey: ... @property def seed(self) -> Field: ... - def sign(self, message: bytes) -> Signature: ... - def sk_sig(self) -> Scalar: ... - def r_sig(self) -> Scalar: ... + def sign(self, message: bytes) -> "Signature": ... + def sk_sig(self) -> "Scalar": ... + def r_sig(self) -> "Scalar": ... + @staticmethod + def from_seed_unchecked(seed: list[int]) -> "PrivateKey": ... + def sign_value(self, message: str) -> "Signature": ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> "PrivateKey": ... + @staticmethod + def new_encrypted(secret: str) -> "PrivateKeyCiphertext": ... + def to_ciphertext(self, secret: str) -> "PrivateKeyCiphertext": ... + @staticmethod + def from_private_key_ciphertext(ciphertext: "PrivateKeyCiphertext", secret: str) -> "PrivateKey": ... class Process: @@ -518,8 +579,20 @@ class Signature: @staticmethod def from_string(s: str) -> Signature: ... @staticmethod - def sign(private_key: PrivateKey, message: bytes) -> Signature: ... - def verify(self, address: Address, message: bytes) -> bool: ... + def sign(private_key: "PrivateKey", message: bytes) -> "Signature": ... + def verify(self, address: "Address", message: bytes) -> bool: ... + def to_address(self) -> "Address": ... + def to_fields(self) -> list["Field"]: ... + def to_bits_le(self) -> list[bool]: ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> "Signature": ... + @staticmethod + def from_bits_le(bits: list[bool]) -> "Signature": ... + def to_plaintext(self) -> "Plaintext": ... + @staticmethod + def sign_value(private_key: "PrivateKey", message: str) -> "Signature": ... + def verify_value(self, address: "Address", message: str) -> bool: ... class Trace: @@ -600,11 +673,25 @@ class U128: class Value: @staticmethod - def parse(s: str) -> Value: ... + def parse(s: str) -> "Value": ... + @staticmethod + def from_literal(literal: "Literal") -> "Value": ... + @staticmethod + def from_record_plaintext(record_plaintext: "RecordPlaintext") -> "Value": ... @staticmethod - def from_literal(literal: Literal) -> Value: ... + def from_plaintext(plaintext: "Plaintext") -> "Value": ... + def to_plaintext(self) -> "Plaintext": ... + def to_record_plaintext(self) -> "RecordPlaintext": ... + def is_plaintext(self) -> bool: ... + def is_record(self) -> bool: ... + def is_future(self) -> bool: ... + @property + def value_type(self) -> str: ... + def bytes(self) -> bytes: ... @staticmethod - def from_record_plaintext(record_plaintext: RecordPlaintext) -> Value: ... + def from_bytes(bytes: bytes) -> "Value": ... + def to_bits_le(self) -> list[bool]: ... + def to_fields(self) -> list["Field"]: ... class VerifyingKey: @@ -616,11 +703,14 @@ class VerifyingKey: class ViewKey: - def decrypt(self, record_ciphertext: RecordCiphertext) -> RecordPlaintext: ... + def decrypt(self, record_ciphertext: "RecordCiphertext") -> "RecordPlaintext": ... @staticmethod - def from_string(s: str) -> ViewKey: ... - def is_owner(self, record_ciphertext: RecordCiphertext) -> bool: ... + def from_string(s: str) -> "ViewKey": ... + def is_owner(self, record_ciphertext: "RecordCiphertext") -> bool: ... @property - def address(self) -> Address: ... - def to_scalar(self) -> Scalar: ... - def to_field(self) -> Field: ... + def address(self) -> "Address": ... + def to_scalar(self) -> "Scalar": ... + def to_field(self) -> "Field": ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> "ViewKey": ... diff --git a/sdk/python/tests/test_account_parity.py b/sdk/python/tests/test_account_parity.py new file mode 100644 index 0000000..9db5788 --- /dev/null +++ b/sdk/python/tests/test_account_parity.py @@ -0,0 +1,316 @@ +""" +Tests for new account-type methods added for W4b parity. + +These tests target methods that extend Address, PrivateKey, PrivateKeyCiphertext, +ViewKey, and Signature beyond the baseline snarkvm bindings. They act as a +specification / acceptance suite: they are expected to fail until the +corresponding Rust pyo3 surface is wired up. +""" + +import pytest +from aleo.mainnet import ( + Address, + Field, + Plaintext, + PrivateKey, + Signature, + ViewKey, +) +from aleo.encryptor import Encryptor + + +# --------------------------------------------------------------------------- +# Known-answer constants +# --------------------------------------------------------------------------- + +CREDITS_PROGRAM_ID = "credits.aleo" +CREDITS_ADDRESS = "aleo1lqmly7ez2k48ajf5hs92ulphaqr05qm4n8qwzj8v0yprmasgpqgsez59gg" + +SEED_UNCHECKED_BYTES = bytes([ + 94, 91, 52, 251, 240, 230, 226, 35, 117, 253, 224, 210, + 175, 13, 205, 120, 155, 214, 7, 169, 66, 62, 206, 50, + 188, 40, 29, 122, 40, 250, 54, 18, +]) +SEED_UNCHECKED_KEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" + +CIPHERTEXT_PK = "APrivateKey1zkpAYS46Dq4rnt9wdohyWMwdmjmTeMJKPZdp5AhvjXZDsVG" +CIPHERTEXT_STR = ( + "ciphertext1qvqg7rgvam3xdcu55pwu6sl8rxwefxaj5gwthk0yzln6jv5fastzup0qn0qftqlqq" + "7jcckyx03fzv9kke0z9puwd7cl7jzyhxfy2f2juplz39dkqs6p24urhxymhv364qm3z8mvyklv5" + "gr52n4fxr2z59jgqytyddj8" +) +CIPHERTEXT_SECRET = "mypassword" + +VALID_ADDRESS = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" + + +# --------------------------------------------------------------------------- +# Address parity +# --------------------------------------------------------------------------- + +class TestAddressParity: + def test_from_program_id_kat(self): + addr = Address.from_program_id(CREDITS_PROGRAM_ID) + assert str(addr) == CREDITS_ADDRESS + + def test_from_program_id_invalid(self): + with pytest.raises(Exception): + Address.from_program_id("not_a_program_id") + + def test_is_valid_true(self): + assert Address.is_valid(VALID_ADDRESS) is True + + def test_is_valid_uppercase(self): + assert Address.is_valid(VALID_ADDRESS.upper()) is True + + def test_is_valid_false(self): + assert Address.is_valid("not_an_address") is False + + def test_is_valid_too_short(self): + assert Address.is_valid("aleo1xyz") is False + + def test_to_bits_le_roundtrip(self): + addr = Address.from_string(VALID_ADDRESS) + bits = addr.to_bits_le() + assert isinstance(bits, list) + assert all(isinstance(b, bool) for b in bits) + addr2 = Address.from_bits_le(bits) + assert addr == addr2 + + def test_to_fields_roundtrip(self): + addr = Address.from_string(VALID_ADDRESS) + fields = addr.to_fields() + assert isinstance(fields, list) + assert len(fields) > 0 + addr2 = Address.from_fields(fields) + assert addr == addr2 + + def test_to_plaintext(self): + addr = Address.from_string(VALID_ADDRESS) + p = addr.to_plaintext() + assert isinstance(p, Plaintext) + assert p.is_literal() + assert p.plaintext_type == "address" + + def test_lossy_casts(self): + addr = Address.from_string(VALID_ADDRESS) + # These just need to not raise. + from aleo.mainnet import Scalar, Boolean + s = addr.to_scalar_lossy() + assert isinstance(s, Scalar) + b = addr.to_boolean_lossy() + assert isinstance(b, Boolean) + assert addr.to_u8_lossy() is not None + assert addr.to_u16_lossy() is not None + assert addr.to_u32_lossy() is not None + assert addr.to_u64_lossy() is not None + assert addr.to_u128_lossy() is not None + assert addr.to_i8_lossy() is not None + assert addr.to_i16_lossy() is not None + assert addr.to_i32_lossy() is not None + assert addr.to_i64_lossy() is not None + assert addr.to_i128_lossy() is not None + + +# --------------------------------------------------------------------------- +# PrivateKey parity +# --------------------------------------------------------------------------- + +class TestPrivateKeyParity: + def test_from_seed_unchecked_kat(self): + pk = PrivateKey.from_seed_unchecked(list(SEED_UNCHECKED_BYTES)) + assert str(pk) == SEED_UNCHECKED_KEY + + def test_from_seed_unchecked_deterministic(self): + seed = list(range(32)) + pk1 = PrivateKey.from_seed_unchecked(seed) + pk2 = PrivateKey.from_seed_unchecked(seed) + assert pk1 == pk2 + + def test_from_seed_unchecked_bad_length(self): + with pytest.raises(Exception): + PrivateKey.from_seed_unchecked([1, 2, 3]) + + def test_sign_value_and_verify(self): + pk = PrivateKey.random() + msg = "42u64" + sig = pk.sign_value(msg) + assert isinstance(sig, Signature) + addr = pk.address + assert sig.verify_value(addr, msg) + assert not sig.verify_value(addr, "43u64") + + def test_bytes_roundtrip(self): + pk = PrivateKey.random() + b = pk.bytes() + pk2 = PrivateKey.from_bytes(b) + assert pk == pk2 + + def test_new_encrypted(self): + ct = PrivateKey.new_encrypted(CIPHERTEXT_SECRET) + assert ct is not None + + def test_to_ciphertext_roundtrip(self): + pk = PrivateKey.random() + ct = pk.to_ciphertext(CIPHERTEXT_SECRET) + pk2 = PrivateKey.from_private_key_ciphertext(ct, CIPHERTEXT_SECRET) + assert pk == pk2 + + def test_from_private_key_ciphertext_wrong_secret(self): + pk = PrivateKey.random() + ct = pk.to_ciphertext(CIPHERTEXT_SECRET) + with pytest.raises(Exception): + PrivateKey.from_private_key_ciphertext(ct, "wrongpassword") + + +# --------------------------------------------------------------------------- +# PrivateKeyCiphertext parity +# +# PrivateKeyCiphertext is a distinct type from the symmetric Ciphertext used by +# Encryptor. It wraps the snarkvm PrivateKeyCiphertext and is exposed as a +# first-class Python type. +# --------------------------------------------------------------------------- + +class TestPrivateKeyCiphertext: + def test_from_string_kat(self): + from aleo.mainnet import PrivateKeyCiphertext + ct = PrivateKeyCiphertext.from_string(CIPHERTEXT_STR) + assert str(ct) == CIPHERTEXT_STR + + def test_decrypt_to_private_key_kat(self): + from aleo.mainnet import PrivateKeyCiphertext + pk_expected = PrivateKey.from_string(CIPHERTEXT_PK) + ct = PrivateKeyCiphertext.from_string(CIPHERTEXT_STR) + pk = ct.decrypt_to_private_key(CIPHERTEXT_SECRET) + assert pk == pk_expected + + def test_decrypt_wrong_secret_fails(self): + from aleo.mainnet import PrivateKeyCiphertext + ct = PrivateKeyCiphertext.from_string(CIPHERTEXT_STR) + with pytest.raises(Exception): + ct.decrypt_to_private_key("badpassword") + + def test_encrypt_decrypt_roundtrip(self): + from aleo.mainnet import PrivateKeyCiphertext + pk = PrivateKey.random() + ct = PrivateKeyCiphertext.encrypt_private_key(pk, CIPHERTEXT_SECRET) + pk2 = ct.decrypt_to_private_key(CIPHERTEXT_SECRET) + assert pk == pk2 + + def test_eq(self): + from aleo.mainnet import PrivateKeyCiphertext + ct1 = PrivateKeyCiphertext.from_string(CIPHERTEXT_STR) + ct2 = PrivateKeyCiphertext.from_string(CIPHERTEXT_STR) + assert ct1 == ct2 + + def test_different_runs_different_ciphertext(self): + """Nonce is random, so encrypting the same key twice yields different ciphertexts.""" + from aleo.mainnet import PrivateKeyCiphertext + pk = PrivateKey.random() + ct1 = PrivateKeyCiphertext.encrypt_private_key(pk, CIPHERTEXT_SECRET) + ct2 = PrivateKeyCiphertext.encrypt_private_key(pk, CIPHERTEXT_SECRET) + assert ct1 != ct2 + + def test_interop_with_python_encryptor(self): + """Python Encryptor and Rust PrivateKeyCiphertext must interoperate. + + The Python Encryptor uses symmetric field-based encryption (Poseidon2 + over the seed field element). PrivateKeyCiphertext uses the same + underlying scheme, so both sides should be able to round-trip through + the other. + """ + from aleo.mainnet import PrivateKeyCiphertext + + pk = PrivateKey.random() + + # Encrypt in Python, decrypt in Rust. + ct_py = Encryptor.encrypt_private_key_with_secret(pk, CIPHERTEXT_SECRET) + ct_rust = PrivateKeyCiphertext.from_string(str(ct_py)) + pk_rust = ct_rust.decrypt_to_private_key(CIPHERTEXT_SECRET) + assert pk == pk_rust + + # Encrypt in Rust, decrypt in Python. + # The Python Encryptor uses aleo.Ciphertext, so convert via string. + from aleo.mainnet import Ciphertext + ct_rust2 = PrivateKeyCiphertext.encrypt_private_key(pk, CIPHERTEXT_SECRET) + ct_as_ciphertext = Ciphertext.from_string(str(ct_rust2)) + pk_py = Encryptor.decrypt_private_key_with_secret(ct_as_ciphertext, CIPHERTEXT_SECRET) + assert pk == pk_py + + +# --------------------------------------------------------------------------- +# ViewKey parity +# --------------------------------------------------------------------------- + +class TestViewKeyParity: + def test_bytes_roundtrip(self): + pk = PrivateKey.random() + vk = pk.view_key + b = vk.bytes() + vk2 = ViewKey.from_bytes(b) + assert vk == vk2 + + def test_bytes_type(self): + pk = PrivateKey.random() + vk = pk.view_key + b = vk.bytes() + assert isinstance(b, list) + assert all(isinstance(x, int) for x in b) + + +# --------------------------------------------------------------------------- +# Signature parity +# --------------------------------------------------------------------------- + +class TestSignatureParity: + def test_to_address(self): + pk = PrivateKey.random() + msg = b"hello world" + sig = Signature.sign(pk, msg) + addr = sig.to_address() + assert isinstance(addr, Address) + assert addr == pk.address + + def test_to_fields(self): + pk = PrivateKey.random() + sig = Signature.sign(pk, b"test") + fields = sig.to_fields() + assert isinstance(fields, list) + assert len(fields) > 0 + assert all(isinstance(f, Field) for f in fields) + + def test_to_bits_le(self): + pk = PrivateKey.random() + sig = Signature.sign(pk, b"test") + bits = sig.to_bits_le() + assert isinstance(bits, list) + assert all(isinstance(b, bool) for b in bits) + + def test_bytes_roundtrip(self): + pk = PrivateKey.random() + sig = Signature.sign(pk, b"test") + b = sig.bytes() + sig2 = Signature.from_bytes(b) + assert sig == sig2 + + def test_from_bits_le_roundtrip(self): + pk = PrivateKey.random() + sig = Signature.sign(pk, b"test") + bits = sig.to_bits_le() + sig2 = Signature.from_bits_le(bits) + assert sig == sig2 + + def test_to_plaintext(self): + pk = PrivateKey.random() + sig = Signature.sign(pk, b"test") + p = sig.to_plaintext() + assert isinstance(p, Plaintext) + assert p.is_literal() + assert p.plaintext_type == "signature" + + def test_sign_value_verify_value(self): + pk = PrivateKey.random() + msg = "100u64" + sig = Signature.sign_value(pk, msg) + assert sig.verify_value(pk.address, msg) + assert not sig.verify_value(pk.address, "99u64") diff --git a/sdk/python/tests/test_value_plaintext.py b/sdk/python/tests/test_value_plaintext.py new file mode 100644 index 0000000..a35cf32 --- /dev/null +++ b/sdk/python/tests/test_value_plaintext.py @@ -0,0 +1,233 @@ +""" +Tests for new Plaintext and Value methods added for W4b parity. + +These tests target methods beyond the baseline snarkvm bindings — find(), +bytes(), to_bits_*, to_bytes_raw_*, to_fields*, plaintext_type, to_python(), +Value.from_plaintext(), Value.is_*, Value.value_type, etc. They act as a +specification / acceptance suite: they are expected to fail until the +corresponding Rust pyo3 surface is wired up. +""" + +import pytest +from aleo.mainnet import Field, Plaintext, RecordPlaintext, Value + + +# --------------------------------------------------------------------------- +# Known-answer strings +# --------------------------------------------------------------------------- + +STRUCT = "{\n microcredits: 100000000u64,\n height: 1653124u32\n}" +NESTED_STRUCT = ( + "{ player: aleo13nnjqa7h2u4mpl95guz97nhzkhlde750zsjnw59tkgdwc85lyurs295lxc," + " health: 100u8," + " inventory: { coins: 5u32, snacks: { candies: 5u64, vegetals: 6u64 } }," + " secret: 2group, cipher: 2scalar, is_alive: true }" +) + +PLAINTEXT_LITERAL = "100u64" +PLAINTEXT_STRUCT = "{\n microcredits: 100000000u64,\n height: 1653124u32\n}" +RECORD_VALUE = ( + "{ owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.private," + " token_amount: 100u64.private," + " _nonce: 0group.public }" +) +FUTURE_VALUE = ( + "{\n" + " program_id: credits.aleo,\n" + " function_name: transfer,\n" + " arguments: [\n" + " aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah,\n" + " 100000000u64\n" + " ]\n" + "}" +) + + +# --------------------------------------------------------------------------- +# Plaintext parity +# --------------------------------------------------------------------------- + +class TestPlaintextParity: + def test_find(self): + p = Plaintext.from_string(STRUCT) + mc = p.find(["microcredits"]) + assert isinstance(mc, Plaintext) + assert mc.is_literal() + + def test_find_nested(self): + p = Plaintext.from_string(NESTED_STRUCT) + inv = p.find(["inventory"]) + assert isinstance(inv, Plaintext) + assert inv.is_struct() + + def test_find_not_found(self): + p = Plaintext.from_string(STRUCT) + with pytest.raises(Exception): + p.find(["nonexistent"]) + + def test_bytes_roundtrip(self): + p = Plaintext.from_string(STRUCT) + b = p.bytes() + p2 = Plaintext.from_bytes(b) + assert p == p2 + + def test_to_bits_le(self): + p = Plaintext.from_string(PLAINTEXT_LITERAL) + bits = p.to_bits_le() + assert isinstance(bits, list) + assert all(isinstance(b, bool) for b in bits) + + def test_to_bits_raw_le(self): + p = Plaintext.from_string(PLAINTEXT_LITERAL) + raw = p.to_bits_raw_le() + assert isinstance(raw, list) + assert all(isinstance(b, bool) for b in raw) + + def test_to_bits_raw_be(self): + p = Plaintext.from_string(PLAINTEXT_LITERAL) + raw = p.to_bits_raw_be() + assert isinstance(raw, list) + assert all(isinstance(b, bool) for b in raw) + + def test_to_bytes_raw_le(self): + p = Plaintext.from_string(PLAINTEXT_LITERAL) + raw = p.to_bytes_raw_le() + assert isinstance(raw, list) + assert all(isinstance(x, int) for x in raw) + + def test_to_bytes_raw_be(self): + p = Plaintext.from_string(PLAINTEXT_LITERAL) + raw = p.to_bytes_raw_be() + assert isinstance(raw, list) + assert all(isinstance(x, int) for x in raw) + + def test_to_fields_roundtrip(self): + p = Plaintext.from_string(STRUCT) + fields = p.to_fields() + assert isinstance(fields, list) + assert len(fields) > 0 + + def test_to_fields_raw(self): + p = Plaintext.from_string(PLAINTEXT_LITERAL) + raw = p.to_fields_raw() + assert isinstance(raw, list) + assert len(raw) > 0 + + def test_plaintext_type_literal(self): + p = Plaintext.from_string("100u64") + assert p.plaintext_type == "u64" + + def test_plaintext_type_struct(self): + p = Plaintext.from_string(STRUCT) + assert p.plaintext_type == "struct" + + def test_plaintext_type_array(self): + p = Plaintext.from_string("[1u8, 2u8, 3u8]") + assert p.plaintext_type == "array" + + def test_to_python_literal_int(self): + p = Plaintext.from_string("42u64") + v = p.to_python() + assert v == 42 + assert isinstance(v, int) + + def test_to_python_literal_bool(self): + p = Plaintext.from_string("true") + v = p.to_python() + assert v is True + assert isinstance(v, bool) + + def test_to_python_struct(self): + p = Plaintext.from_string(STRUCT) + v = p.to_python() + assert isinstance(v, dict) + assert "microcredits" in v + assert v["microcredits"] == 100_000_000 + + def test_to_python_nested_struct(self): + p = Plaintext.from_string(NESTED_STRUCT) + v = p.to_python() + assert isinstance(v, dict) + assert "inventory" in v + assert isinstance(v["inventory"], dict) + + def test_to_python_array(self): + p = Plaintext.from_string("[1u8, 2u8, 3u8]") + v = p.to_python() + assert isinstance(v, list) + assert len(v) == 3 + assert v[0] == 1 + + def test_to_python_address_str(self): + addr = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" + p = Plaintext.from_string(addr) + v = p.to_python() + assert isinstance(v, str) + assert addr in v + + +# --------------------------------------------------------------------------- +# Value parity +# --------------------------------------------------------------------------- + +class TestValueParity: + def test_from_plaintext_literal(self): + p = Plaintext.from_string("100u64") + v = Value.from_plaintext(p) + assert isinstance(v, Value) + assert v.is_plaintext() + assert v.value_type == "plaintext" + + def test_to_plaintext_roundtrip(self): + p = Plaintext.from_string("100u64") + v = Value.from_plaintext(p) + p2 = v.to_plaintext() + assert p == p2 + + def test_to_plaintext_from_non_plaintext_fails(self): + v = Value.parse(RECORD_VALUE) + with pytest.raises(Exception): + v.to_plaintext() + + def test_to_record_plaintext(self): + v = Value.parse(RECORD_VALUE) + assert v.is_record() + r = v.to_record_plaintext() + assert isinstance(r, RecordPlaintext) + + def test_is_future(self): + v = Value.parse(FUTURE_VALUE) + assert v.is_future() + assert not v.is_plaintext() + assert not v.is_record() + + def test_value_type_plaintext(self): + v = Value.parse(PLAINTEXT_LITERAL) + assert v.value_type == "plaintext" + + def test_value_type_record(self): + v = Value.parse(RECORD_VALUE) + assert v.value_type == "record" + + def test_value_type_future(self): + v = Value.parse(FUTURE_VALUE) + assert v.value_type == "future" + + def test_bytes_roundtrip(self): + v = Value.parse(PLAINTEXT_LITERAL) + b = v.bytes() + v2 = Value.from_bytes(b) + assert str(v) == str(v2) + + def test_to_bits_le(self): + v = Value.parse(PLAINTEXT_LITERAL) + bits = v.to_bits_le() + assert isinstance(bits, list) + assert all(isinstance(b, bool) for b in bits) + + def test_to_fields(self): + v = Value.parse(PLAINTEXT_LITERAL) + fields = v.to_fields() + assert isinstance(fields, list) + assert len(fields) > 0 + assert all(isinstance(f, Field) for f in fields) diff --git a/sdk/src/account/address.rs b/sdk/src/account/address.rs index e130592..572668e 100644 --- a/sdk/src/account/address.rs +++ b/sdk/src/account/address.rs @@ -14,10 +14,17 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::{types::AddressNative, Field, Group}; +use crate::{ + types::{ + AddressNative, BooleanNative, FieldNative, I128Native, I16Native, I32Native, I64Native, + I8Native, ProgramIDNative, ScalarNative, U128Native, U16Native, U32Native, U64Native, + U8Native, + }, + Boolean, Field, Group, Plaintext, Scalar, I128, I16, I32, I64, I8, U128, U16, U32, U64, U8, +}; use pyo3::prelude::*; -use snarkvm::prelude::ToField; +use snarkvm::prelude::{FromBits, FromFields, Network, ToBits, ToField, ToFields}; use std::{ collections::hash_map::DefaultHasher, @@ -76,6 +83,115 @@ impl Address { Ok(Self(AddressNative::new(**group))) } + /// Returns the address of a program based on the program ID string. + #[staticmethod] + pub fn from_program_id(program_id: &str) -> anyhow::Result<Self> { + let program_id_native = ProgramIDNative::from_str(program_id)?; + let name_field = program_id_native.name().to_field()?; + let network_field = program_id_native.network().to_field()?; + let group = <crate::types::CurrentNetwork as Network>::hash_to_group_psd4(&[ + name_field, + network_field, + ])?; + Ok(Self(AddressNative::new(group))) + } + + /// Returns true if the string is a valid Aleo address (auto-lowercased before validation). + #[staticmethod] + pub fn is_valid(address: &str) -> bool { + AddressNative::from_str(&address.to_lowercase()).is_ok() + } + + /// Returns the little-endian bit representation of the address. + pub fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Recovers an address from little-endian bits. + #[staticmethod] + pub fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + AddressNative::from_bits_le(&bits).map(Self) + } + + /// Returns the field element encoding of the address. + pub fn to_fields(&self) -> anyhow::Result<Vec<Field>> { + Ok(self.0.to_fields()?.into_iter().map(Into::into).collect()) + } + + /// Recovers an address from field elements. + #[staticmethod] + pub fn from_fields(fields: Vec<Field>) -> anyhow::Result<Self> { + let native: Vec<FieldNative> = fields.into_iter().map(Into::into).collect(); + AddressNative::from_fields(&native).map(Self) + } + + /// Returns the address wrapped as a Plaintext::Literal(Address). + pub fn to_plaintext(&self) -> Plaintext { + use crate::types::{LiteralNative, PlaintextNative}; + Plaintext::from(PlaintextNative::from(LiteralNative::Address(self.0))) + } + + /// Cast the address to a Scalar with lossy truncation (via x-coordinate). + pub fn to_scalar_lossy(&self) -> Scalar { + ScalarNative::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to a Boolean (LSB of x-coordinate). + pub fn to_boolean_lossy(&self) -> Boolean { + let bit = self.0.to_group().to_x_coordinate().to_bits_le()[0]; + BooleanNative::new(bit).into() + } + + /// Cast the address to a U8 with lossy truncation. + pub fn to_u8_lossy(&self) -> U8 { + U8Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to a U16 with lossy truncation. + pub fn to_u16_lossy(&self) -> U16 { + U16Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to a U32 with lossy truncation. + pub fn to_u32_lossy(&self) -> U32 { + U32Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to a U64 with lossy truncation. + pub fn to_u64_lossy(&self) -> U64 { + U64Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to a U128 with lossy truncation. + pub fn to_u128_lossy(&self) -> U128 { + U128Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to an I8 with lossy truncation. + pub fn to_i8_lossy(&self) -> I8 { + I8Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to an I16 with lossy truncation. + pub fn to_i16_lossy(&self) -> I16 { + I16Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to an I32 with lossy truncation. + pub fn to_i32_lossy(&self) -> I32 { + I32Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to an I64 with lossy truncation. + pub fn to_i64_lossy(&self) -> I64 { + I64Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + + /// Cast the address to an I128 with lossy truncation. + pub fn to_i128_lossy(&self) -> I128 { + I128Native::from_field_lossy(&self.0.to_group().to_x_coordinate()).into() + } + /// Returns the address as a base58 string. fn __str__(&self) -> String { self.0.to_string() diff --git a/sdk/src/account/mod.rs b/sdk/src/account/mod.rs index 4392474..fea2ffc 100644 --- a/sdk/src/account/mod.rs +++ b/sdk/src/account/mod.rs @@ -26,6 +26,9 @@ pub use graph_key::GraphKey; mod private_key; pub use private_key::PrivateKey; +mod private_key_ciphertext; +pub use private_key_ciphertext::PrivateKeyCiphertext; + mod record; pub use record::{RecordCiphertext, RecordPlaintext}; diff --git a/sdk/src/account/private_key.rs b/sdk/src/account/private_key.rs index f6963bd..5536019 100644 --- a/sdk/src/account/private_key.rs +++ b/sdk/src/account/private_key.rs @@ -15,12 +15,16 @@ // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. use crate::{ - types::{AddressNative, ComputeKeyNative, PrivateKeyNative, ViewKeyNative}, - Address, ComputeKey, Field, Scalar, Signature, ViewKey, + types::{ + AddressNative, ComputeKeyNative, CurrentNetwork, PrivateKeyNative, ValueNative, + ViewKeyNative, + }, + Address, ComputeKey, Field, PrivateKeyCiphertext, Scalar, Signature, ViewKey, }; use pyo3::prelude::*; use rand::rngs::StdRng; +use snarkvm::prelude::ToFields; use std::{ collections::hash_map::DefaultHasher, @@ -62,6 +66,19 @@ impl PrivateKey { PrivateKeyNative::try_from(seed.into()).map(Self) } + /// Returns a private key from a 32-byte seed (unchecked; uses LE-mod-order reduction). + #[staticmethod] + pub fn from_seed_unchecked(seed: Vec<u8>) -> anyhow::Result<Self> { + #[allow(unused_imports)] + use snarkvm::prelude::*; + let seed: [u8; 32] = seed + .try_into() + .map_err(|_| anyhow::anyhow!("seed must be exactly 32 bytes"))?; + let inner = <CurrentNetwork as Environment>::Field::from_bytes_le_mod_order(&seed); + let field = crate::types::FieldNative::new(inner); + Ok(Self(PrivateKeyNative::try_from(field)?)) + } + /// Reads in an account private key from a base58 string. #[staticmethod] fn from_string(private_key: &str) -> anyhow::Result<Self> { @@ -70,7 +87,7 @@ impl PrivateKey { /// Returns the account seed. #[getter] - fn seed(&self) -> Field { + pub fn seed(&self) -> Field { self.0.seed().into() } @@ -79,6 +96,51 @@ impl PrivateKey { Signature::sign(self, message) } + /// Signs a Value-domain message (any valid Aleo literal, struct, array, or record). + pub fn sign_value(&self, message: &str) -> anyhow::Result<Signature> { + let value = ValueNative::from_str(message)?; + let fields = value.to_fields()?; + Ok(self + .0 + .sign(&fields, &mut rand::make_rng::<StdRng>())? + .into()) + } + + /// Returns the little-endian byte representation of the private key. + pub fn bytes(&self) -> anyhow::Result<Vec<u8>> { + use snarkvm::prelude::ToBytes; + self.0.to_bytes_le() + } + + /// Recovers a private key from its little-endian byte representation. + #[staticmethod] + pub fn from_bytes(bytes: Vec<u8>) -> anyhow::Result<Self> { + use snarkvm::prelude::FromBytes; + Ok(Self(PrivateKeyNative::read_le(&bytes[..])?)) + } + + /// Generates a new random private key and returns it encrypted with the given secret. + #[staticmethod] + pub fn new_encrypted(secret: &str) -> anyhow::Result<PrivateKeyCiphertext> { + let key = Self::random(); + PrivateKeyCiphertext::encrypt_private_key(&key, secret) + } + + /// Encrypts this private key with the given secret and returns the ciphertext. + #[allow(clippy::wrong_self_convention)] + pub fn to_ciphertext(&self, secret: &str) -> anyhow::Result<PrivateKeyCiphertext> { + PrivateKeyCiphertext::encrypt_private_key(self, secret) + } + + /// Decrypts a PrivateKeyCiphertext with the given secret and returns the private key. + #[staticmethod] + pub fn from_private_key_ciphertext( + ciphertext: &PrivateKeyCiphertext, + secret: &str, + ) -> anyhow::Result<Self> { + ciphertext.decrypt_to_private_key(secret) + } + /// Returns the signature secret key. fn sk_sig(&self) -> Scalar { self.0.sk_sig().into() diff --git a/sdk/src/account/private_key_ciphertext.rs b/sdk/src/account/private_key_ciphertext.rs new file mode 100644 index 0000000..e21307f --- /dev/null +++ b/sdk/src/account/private_key_ciphertext.rs @@ -0,0 +1,145 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + types::{ + CiphertextNative, CurrentNetwork, FieldNative, IdentifierNative, LiteralNative, + PlaintextNative, PrivateKeyNative, + }, + PrivateKey, +}; + +use pyo3::prelude::*; +use rand::rngs::StdRng; +use snarkvm::prelude::{Network, Uniform}; + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + ops::Deref, + str::FromStr, + sync::OnceLock, +}; + +/// Private key encrypted into ciphertext using a secret. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct PrivateKeyCiphertext(CiphertextNative); + +impl PrivateKeyCiphertext { + /// Internal: encrypts a field element with domain separation. + fn encrypt_field( + field: &FieldNative, + secret: &str, + domain: &str, + ) -> anyhow::Result<CiphertextNative> { + let domain_f = FieldNative::new_domain_separator(domain); + let secret_f = FieldNative::new_domain_separator(secret); + let nonce = <FieldNative as Uniform>::rand(&mut rand::make_rng::<StdRng>()); + let blinding = CurrentNetwork::hash_psd2(&[domain_f, nonce, secret_f])?; + let key = blinding * field; + let plaintext = PlaintextNative::Struct( + indexmap::IndexMap::from_iter(vec![ + ( + IdentifierNative::from_str("key")?, + PlaintextNative::Literal(LiteralNative::Field(key), OnceLock::new()), + ), + ( + IdentifierNative::from_str("nonce")?, + PlaintextNative::Literal(LiteralNative::Field(nonce), OnceLock::new()), + ), + ]), + OnceLock::new(), + ); + plaintext.encrypt_symmetric(secret_f) + } + + /// Internal: decrypts a field element from ciphertext. + fn decrypt_field( + ciphertext: &CiphertextNative, + secret: &str, + domain: &str, + ) -> anyhow::Result<FieldNative> { + let domain_f = FieldNative::new_domain_separator(domain); + let secret_f = FieldNative::new_domain_separator(secret); + let decrypted = ciphertext.decrypt_symmetric(secret_f)?; + let recovered_key = Self::extract_field(&decrypted, "key")?; + let recovered_nonce = Self::extract_field(&decrypted, "nonce")?; + let recovered_blinding = CurrentNetwork::hash_psd2(&[domain_f, recovered_nonce, secret_f])?; + Ok(recovered_key / recovered_blinding) + } + + /// Internal: extracts a Field literal from a struct plaintext member. + fn extract_field(plaintext: &PlaintextNative, identifier: &str) -> anyhow::Result<FieldNative> { + let id = IdentifierNative::from_str(identifier)?; + let value = plaintext.find(&[id])?; + match value { + PlaintextNative::Literal(LiteralNative::Field(f), _) => Ok(f), + _ => anyhow::bail!("expected field literal for '{identifier}'"), + } + } +} + +#[pymethods] +impl PrivateKeyCiphertext { + /// Encrypts a private key with a secret string. + #[staticmethod] + pub fn encrypt_private_key(private_key: &PrivateKey, secret: &str) -> anyhow::Result<Self> { + let seed = (*private_key).seed(); + Ok(Self(Self::encrypt_field(&seed, secret, "private_key")?)) + } + + /// Decrypts self into a private key using the given secret. + pub fn decrypt_to_private_key(&self, secret: &str) -> anyhow::Result<PrivateKey> { + let seed = Self::decrypt_field(&self.0, secret, "private_key")?; + Ok(PrivateKeyNative::try_from(seed)?.into()) + } + + /// Parses a ciphertext from its string representation. + #[staticmethod] + pub fn from_string(s: &str) -> anyhow::Result<Self> { + CiphertextNative::from_str(s).map(Self) + } + + /// Returns the string representation of the ciphertext. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } + + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.to_string().hash(&mut hasher); + hasher.finish() + } +} + +impl Deref for PrivateKeyCiphertext { + type Target = CiphertextNative; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<CiphertextNative> for PrivateKeyCiphertext { + fn from(value: CiphertextNative) -> Self { + Self(value) + } +} diff --git a/sdk/src/account/signature.rs b/sdk/src/account/signature.rs index 18a00dd..0a20abb 100644 --- a/sdk/src/account/signature.rs +++ b/sdk/src/account/signature.rs @@ -14,10 +14,14 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::{types::SignatureNative, Address, ComputeKey, PrivateKey, Scalar}; +use crate::{ + types::{LiteralNative, SignatureNative, ValueNative}, + Address, ComputeKey, Field, Plaintext, PrivateKey, Scalar, +}; use pyo3::prelude::*; use rand::rngs::StdRng; +use snarkvm::prelude::{FromBits, FromBytes, ToBits, ToBytes, ToFields}; use std::{ collections::hash_map::DefaultHasher, @@ -71,6 +75,63 @@ impl Signature { self.0.verify_bytes(address, message) } + /// Returns the signer address derived from the compute key embedded in this signature. + pub fn to_address(&self) -> Address { + self.0.to_address().into() + } + + /// Returns the field elements encoding this signature. + pub fn to_fields(&self) -> anyhow::Result<Vec<Field>> { + Ok(self.0.to_fields()?.into_iter().map(Into::into).collect()) + } + + /// Returns the little-endian bit representation of the signature. + pub fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Returns the little-endian byte representation of the signature. + pub fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Recovers a signature from its little-endian byte representation. + #[staticmethod] + pub fn from_bytes(bytes: Vec<u8>) -> anyhow::Result<Self> { + Ok(Self(SignatureNative::read_le(&bytes[..])?)) + } + + /// Recovers a signature from little-endian bits. + #[staticmethod] + pub fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + SignatureNative::from_bits_le(&bits).map(Self) + } + + /// Returns the signature wrapped as a Plaintext::Literal(Signature). + pub fn to_plaintext(&self) -> Plaintext { + use crate::types::PlaintextNative; + Plaintext::from(PlaintextNative::from(LiteralNative::Signature(Box::new( + self.0, + )))) + } + + /// Signs a Value-domain message (any valid Aleo literal, struct, array, or record). + #[staticmethod] + pub fn sign_value(private_key: &PrivateKey, message: &str) -> anyhow::Result<Self> { + let value = ValueNative::from_str(message)?; + let fields = value.to_fields()?; + Ok(Self( + (**private_key).sign(&fields, &mut rand::make_rng::<StdRng>())?, + )) + } + + /// Verifies a Value-domain signature against an address. + pub fn verify_value(&self, address: &Address, message: &str) -> anyhow::Result<bool> { + let value = ValueNative::from_str(message)?; + let fields = value.to_fields()?; + Ok(self.0.verify(address, &fields)) + } + /// Returns a string representation of the signature. fn __str__(&self) -> String { self.0.to_string() diff --git a/sdk/src/account/text.rs b/sdk/src/account/text.rs index 3855408..1e1e9e7 100644 --- a/sdk/src/account/text.rs +++ b/sdk/src/account/text.rs @@ -15,13 +15,16 @@ // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. use crate::{ - types::{CiphertextNative, LiteralNative, PlaintextNative}, + types::{CiphertextNative, FieldNative, IdentifierNative, LiteralNative, PlaintextNative}, Address, Field, Group, Identifier, Literal, Scalar, ViewKey, }; use std::ops::Deref; use pyo3::{exceptions::PyTypeError, prelude::*}; +use snarkvm::prelude::{ + FromBits, FromBytes, FromFields, ToBits, ToBitsRaw, ToBytes, ToFields, ToFieldsRaw, +}; use std::{collections::HashMap, str::FromStr}; /// The Aleo ciphertext type. @@ -172,6 +175,113 @@ impl Plaintext { } } + /// Finds a member in a struct plaintext by name. + /// Returns an error if the plaintext is not a struct or the member doesn't exist. + pub fn find(&self, path: Vec<String>) -> anyhow::Result<Self> { + let identifiers: Vec<IdentifierNative> = path + .iter() + .map(|s| IdentifierNative::from_str(s)) + .collect::<Result<Vec<_>, _>>()?; + Ok(Self(self.0.find(&identifiers)?)) + } + + /// Returns the byte representation of the plaintext (little-endian). + pub fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Parses a plaintext from its little-endian byte representation. + #[staticmethod] + pub fn from_bytes(bytes: Vec<u8>) -> anyhow::Result<Self> { + PlaintextNative::from_bytes_le(&bytes).map(Self) + } + + /// Returns the little-endian bit representation of the plaintext. + pub fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Returns the raw little-endian bit representation (no length prefix). + pub fn to_bits_raw_le(&self) -> Vec<bool> { + self.0.to_bits_raw_le() + } + + /// Returns the raw big-endian bit representation (no length prefix). + pub fn to_bits_raw_be(&self) -> Vec<bool> { + self.0.to_bits_raw_be() + } + + /// Returns the raw little-endian byte representation (packed from raw LE bits). + pub fn to_bytes_raw_le(&self) -> anyhow::Result<Vec<u8>> { + let bits_le = self.0.to_bits_raw_le(); + let bytes: Vec<u8> = bits_le + .chunks(8) + .map(|chunk| { + crate::types::U8Native::from_bits_le(chunk) + .map(|u8_val| u8_val.to_bytes_le().unwrap()[0]) + }) + .collect::<Result<Vec<_>, _>>()?; + Ok(bytes) + } + + /// Returns the raw big-endian byte representation (packed from raw BE bits). + pub fn to_bytes_raw_be(&self) -> anyhow::Result<Vec<u8>> { + let bits_be = self.0.to_bits_raw_be(); + let mut bytes: Vec<u8> = bits_be + .chunks(8) + .map(|chunk| { + crate::types::U8Native::from_bits_be(chunk) + .map(|u8_val| u8_val.to_bytes_le().unwrap()[0]) + }) + .collect::<Result<Vec<_>, _>>()?; + bytes.reverse(); + Ok(bytes) + } + + /// Returns the field element encoding of the plaintext. + pub fn to_fields(&self) -> anyhow::Result<Vec<Field>> { + Ok(self.0.to_fields()?.into_iter().map(Into::into).collect()) + } + + /// Recovers a plaintext from field elements. + #[staticmethod] + pub fn from_fields(fields: Vec<Field>) -> anyhow::Result<Self> { + let native: Vec<FieldNative> = fields.into_iter().map(Into::into).collect(); + PlaintextNative::from_fields(&native).map(Self) + } + + /// Returns the raw field element encoding of the plaintext (no metadata). + pub fn to_fields_raw(&self) -> anyhow::Result<Vec<Field>> { + Ok(self + .0 + .to_fields_raw()? + .into_iter() + .map(Into::into) + .collect()) + } + + /// Returns the type of the plaintext: the literal type name, "struct", or "array". + #[getter] + pub fn plaintext_type(&self) -> String { + match &self.0 { + PlaintextNative::Literal(literal, _) => literal.to_type().type_name().to_string(), + PlaintextNative::Struct(..) => "struct".to_string(), + PlaintextNative::Array(..) => "array".to_string(), + } + } + + /// Converts the plaintext to a native Python object. + /// + /// Mapping: + /// - Literal(boolean) → bool + /// - Literal(u8/u16/u32/u64/u128/i8/i16/i32/i64/i128) → int + /// - Literal(address/field/group/scalar/signature) → str + /// - Struct → dict[str, <recursive>] + /// - Array → list[<recursive>] + pub fn to_python(&self, py: Python<'_>) -> PyObject { + plaintext_to_pyobject(&self.0, py) + } + /// Returns the plaintext as a string. fn __str__(&self) -> String { self.0.to_string() @@ -201,3 +311,33 @@ impl From<Plaintext> for PlaintextNative { value.0 } } + +fn plaintext_to_pyobject(p: &PlaintextNative, py: Python<'_>) -> PyObject { + match p { + PlaintextNative::Literal(lit, _) => match lit { + LiteralNative::Boolean(b) => (**b).into_py(py), + LiteralNative::U8(n) => (**n).into_py(py), + LiteralNative::U16(n) => (**n).into_py(py), + LiteralNative::U32(n) => (**n).into_py(py), + LiteralNative::U64(n) => (**n).into_py(py), + LiteralNative::U128(n) => (**n).into_py(py), + LiteralNative::I8(n) => (**n).into_py(py), + LiteralNative::I16(n) => (**n).into_py(py), + LiteralNative::I32(n) => (**n).into_py(py), + LiteralNative::I64(n) => (**n).into_py(py), + LiteralNative::I128(n) => (**n).into_py(py), + _ => lit.to_string().into_py(py), + }, + PlaintextNative::Struct(members, _) => { + let dict = pyo3::types::PyDict::new(py); + for (k, v) in members.iter() { + let _ = dict.set_item(k.to_string(), plaintext_to_pyobject(v, py)); + } + dict.into_py(py) + } + PlaintextNative::Array(elems, _) => { + let list: Vec<PyObject> = elems.iter().map(|e| plaintext_to_pyobject(e, py)).collect(); + list.into_py(py) + } + } +} diff --git a/sdk/src/account/view_key.rs b/sdk/src/account/view_key.rs index b08ee74..a75bb02 100644 --- a/sdk/src/account/view_key.rs +++ b/sdk/src/account/view_key.rs @@ -17,7 +17,7 @@ use crate::{types::ViewKeyNative, Address, Field, RecordCiphertext, RecordPlaintext, Scalar}; use pyo3::prelude::*; -use snarkvm::prelude::ToField; +use snarkvm::prelude::{FromBytes, ToBytes, ToField}; use std::{ collections::hash_map::DefaultHasher, @@ -65,6 +65,17 @@ impl ViewKey { self.0.to_field().map(Into::into) } + /// Returns the little-endian byte representation of the view key. + pub fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Recovers a view key from its little-endian byte representation. + #[staticmethod] + pub fn from_bytes(bytes: Vec<u8>) -> anyhow::Result<Self> { + Ok(Self(ViewKeyNative::read_le(&bytes[..])?)) + } + /// Returns the view key as a base58 string. fn __str__(&self) -> String { self.0.to_string() diff --git a/sdk/src/algebra/boolean.rs b/sdk/src/algebra/boolean.rs index 13dced5..0461bea 100644 --- a/sdk/src/algebra/boolean.rs +++ b/sdk/src/algebra/boolean.rs @@ -60,3 +60,9 @@ impl From<Boolean> for BooleanNative { value.0 } } + +impl From<BooleanNative> for Boolean { + fn from(value: BooleanNative) -> Self { + Self(value) + } +} diff --git a/sdk/src/algebra/integer.rs b/sdk/src/algebra/integer.rs index 93da6f9..bf47058 100644 --- a/sdk/src/algebra/integer.rs +++ b/sdk/src/algebra/integer.rs @@ -71,6 +71,12 @@ macro_rules! integer { value.0 } } + + impl From<$native> for $export_ty { + fn from(value: $native) -> Self { + Self(value) + } + } }; } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 38568ea..e4be2b8 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -68,6 +68,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Pedersen64>()?; m.add_class::<Pedersen128>()?; m.add_class::<Plaintext>()?; + m.add_class::<PrivateKeyCiphertext>()?; m.add_class::<Poseidon2>()?; m.add_class::<Poseidon4>()?; m.add_class::<Poseidon8>()?; diff --git a/sdk/src/programs/value.rs b/sdk/src/programs/value.rs index d9a02f3..c0f8f38 100644 --- a/sdk/src/programs/value.rs +++ b/sdk/src/programs/value.rs @@ -15,9 +15,10 @@ // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. use crate::{ - types::{LiteralNative, RecordPlaintextNative, ValueNative}, - Literal, RecordPlaintext, + types::{LiteralNative, PlaintextNative, RecordPlaintextNative, ValueNative}, + Field, Literal, Plaintext, RecordPlaintext, }; +use snarkvm::prelude::{FromBytes, ToBits, ToBytes, ToFields}; use pyo3::prelude::*; @@ -50,6 +51,78 @@ impl Value { ))) } + /// Creates a Value wrapping the given Plaintext. + #[staticmethod] + pub fn from_plaintext(plaintext: &Plaintext) -> Self { + use std::ops::Deref; + let native: PlaintextNative = plaintext.deref().clone(); + Self(ValueNative::Plaintext(native)) + } + + /// Extracts the inner Plaintext. Errors if this is not a Plaintext variant. + pub fn to_plaintext(&self) -> anyhow::Result<Plaintext> { + match &self.0 { + ValueNative::Plaintext(p) => Ok(Plaintext::from(p.clone())), + _ => anyhow::bail!("Value is not a Plaintext variant"), + } + } + + /// Extracts the inner Record as a RecordPlaintext. Errors if not a Record variant. + pub fn to_record_plaintext(&self) -> anyhow::Result<RecordPlaintext> { + match &self.0 { + ValueNative::Record(r) => Ok(RecordPlaintext::from(r.clone())), + _ => anyhow::bail!("Value is not a Record variant"), + } + } + + /// Returns true if this value is a Plaintext variant. + pub fn is_plaintext(&self) -> bool { + matches!(&self.0, ValueNative::Plaintext(..)) + } + + /// Returns true if this value is a Record variant. + pub fn is_record(&self) -> bool { + matches!(&self.0, ValueNative::Record(..)) + } + + /// Returns true if this value is a Future variant. + pub fn is_future(&self) -> bool { + matches!(&self.0, ValueNative::Future(..)) + } + + /// Returns the variant type as a string: "plaintext", "record", "future", "dynamic_record", or "dynamic_future". + #[getter] + pub fn value_type(&self) -> String { + match &self.0 { + ValueNative::Plaintext(..) => "plaintext".to_string(), + ValueNative::Record(..) => "record".to_string(), + ValueNative::Future(..) => "future".to_string(), + ValueNative::DynamicRecord(..) => "dynamic_record".to_string(), + ValueNative::DynamicFuture(..) => "dynamic_future".to_string(), + } + } + + /// Returns the byte representation of the value (little-endian). + pub fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Recovers a Value from its little-endian byte representation. + #[staticmethod] + pub fn from_bytes(bytes: Vec<u8>) -> anyhow::Result<Self> { + Ok(Self(ValueNative::read_le(&bytes[..])?)) + } + + /// Returns the little-endian bit representation. + pub fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Returns the field element encoding of the value. + pub fn to_fields(&self) -> anyhow::Result<Vec<Field>> { + Ok(self.0.to_fields()?.into_iter().map(Into::into).collect()) + } + /// Returns the value as a string. fn __str__(&self) -> String { self.0.to_string() From 96b42786afcbee006d025ab054e45add8f53de4b Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 07:55:37 -0400 Subject: [PATCH 38/72] fix: mirror W4b surface into __init__.pyi; strengthen interop and lossy-cast tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 90 +++++++++++++++++++++++++ sdk/python/tests/test_account_parity.py | 89 +++++++++++++++--------- 2 files changed, 149 insertions(+), 30 deletions(-) diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index 2ba9cbd..42dea50 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -26,9 +26,32 @@ class Address: def from_group(group: Group) -> Address: ... @staticmethod def from_bytes_le(bytes: bytes) -> Address: ... + @staticmethod + def from_program_id(program_id: str) -> Address: ... + @staticmethod + def is_valid(address: str) -> bool: ... def to_group(self) -> Group: ... def to_field(self) -> Field: ... def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> Address: ... + def to_fields(self) -> List[Field]: ... + @staticmethod + def from_fields(fields: List[Field]) -> Address: ... + def to_plaintext(self) -> Plaintext: ... + def to_scalar_lossy(self) -> Scalar: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class Authorization: @@ -348,6 +371,33 @@ class Plaintext: def as_literal(self) -> Literal: ... def as_struct(self) -> Mapping[Identifier, Plaintext]: ... def as_array(self) -> List[Plaintext]: ... + def find(self, path: List[str]) -> Plaintext: ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> Plaintext: ... + def to_bits_le(self) -> List[bool]: ... + def to_bits_raw_le(self) -> List[bool]: ... + def to_bits_raw_be(self) -> List[bool]: ... + def to_bytes_raw_le(self) -> bytes: ... + def to_bytes_raw_be(self) -> bytes: ... + def to_fields(self) -> List[Field]: ... + @staticmethod + def from_fields(fields: List[Field]) -> Plaintext: ... + def to_fields_raw(self) -> List[Field]: ... + @property + def plaintext_type(self) -> str: ... + def to_python(self) -> object: ... + + +class PrivateKeyCiphertext: + @staticmethod + def encrypt_private_key(private_key: PrivateKey, secret: str) -> PrivateKeyCiphertext: ... + def decrypt_to_private_key(self, secret: str) -> PrivateKey: ... + @staticmethod + def from_string(s: str) -> PrivateKeyCiphertext: ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... class Poseidon2: @@ -398,6 +448,17 @@ class PrivateKey: def sign(self, message: bytes) -> Signature: ... def sk_sig(self) -> Scalar: ... def r_sig(self) -> Scalar: ... + @staticmethod + def from_seed_unchecked(seed: List[int]) -> PrivateKey: ... + def sign_value(self, message: str) -> Signature: ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> PrivateKey: ... + @staticmethod + def new_encrypted(secret: str) -> PrivateKeyCiphertext: ... + def to_ciphertext(self, secret: str) -> PrivateKeyCiphertext: ... + @staticmethod + def from_private_key_ciphertext(ciphertext: PrivateKeyCiphertext, secret: str) -> PrivateKey: ... class Process: @@ -533,6 +594,18 @@ class Signature: @staticmethod def sign(private_key: PrivateKey, message: bytes) -> Signature: ... def verify(self, address: Address, message: bytes) -> bool: ... + def to_address(self) -> Address: ... + def to_fields(self) -> List[Field]: ... + def to_bits_le(self) -> List[bool]: ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> Signature: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> Signature: ... + def to_plaintext(self) -> Plaintext: ... + @staticmethod + def sign_value(private_key: PrivateKey, message: str) -> Signature: ... + def verify_value(self, address: Address, message: str) -> bool: ... class Trace: @@ -618,6 +691,20 @@ class Value: def from_literal(literal: Literal) -> Value: ... @staticmethod def from_record_plaintext(record_plaintext: RecordPlaintext) -> Value: ... + @staticmethod + def from_plaintext(plaintext: Plaintext) -> Value: ... + def to_plaintext(self) -> Plaintext: ... + def to_record_plaintext(self) -> RecordPlaintext: ... + def is_plaintext(self) -> bool: ... + def is_record(self) -> bool: ... + def is_future(self) -> bool: ... + @property + def value_type(self) -> str: ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> Value: ... + def to_bits_le(self) -> List[bool]: ... + def to_fields(self) -> List[Field]: ... class VerifyingKey: @@ -637,3 +724,6 @@ class ViewKey: def address(self) -> Address: ... def to_scalar(self) -> Scalar: ... def to_field(self) -> Field: ... + def bytes(self) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes) -> ViewKey: ... diff --git a/sdk/python/tests/test_account_parity.py b/sdk/python/tests/test_account_parity.py index 9db5788..16bc3c9 100644 --- a/sdk/python/tests/test_account_parity.py +++ b/sdk/python/tests/test_account_parity.py @@ -93,23 +93,38 @@ def test_to_plaintext(self): assert p.plaintext_type == "address" def test_lossy_casts(self): + # Concrete expected values pinned as regression anchors. + # These are deterministic derivations from VALID_ADDRESS — computed once + # and hardcoded so future refactors cannot silently change the output. + # Each lossy cast truncates the 256-bit address encoding to the target + # type's bit-width; the exact values were observed on 2026-07-09. addr = Address.from_string(VALID_ADDRESS) - # These just need to not raise. from aleo.mainnet import Scalar, Boolean + + # to_field: address x-coordinate (full precision) + assert str(addr.to_field()) == "3501665755452795161867664882580888971213780722176652848275908626939553697821field" + + # to_scalar_lossy: 252-bit truncation into the scalar field s = addr.to_scalar_lossy() assert isinstance(s, Scalar) + assert str(s) == "1692414361119729608374368241820140411006437211776019035159383876815911047197scalar" + + # to_boolean_lossy: least significant bit of the encoding b = addr.to_boolean_lossy() assert isinstance(b, Boolean) - assert addr.to_u8_lossy() is not None - assert addr.to_u16_lossy() is not None - assert addr.to_u32_lossy() is not None - assert addr.to_u64_lossy() is not None - assert addr.to_u128_lossy() is not None - assert addr.to_i8_lossy() is not None - assert addr.to_i16_lossy() is not None - assert addr.to_i32_lossy() is not None - assert addr.to_i64_lossy() is not None - assert addr.to_i128_lossy() is not None + assert str(b) == "true" + + # integer lossy casts: truncation to N-bit two's-complement / unsigned + assert str(addr.to_u8_lossy()) == "29u8" + assert str(addr.to_u16_lossy()) == "53277u16" + assert str(addr.to_u32_lossy()) == "2078199837u32" + assert str(addr.to_u64_lossy()) == "15564512705944408093u64" + assert str(addr.to_u128_lossy()) == "34922309281260474190457069241198628893u128" + assert str(addr.to_i8_lossy()) == "29i8" + assert str(addr.to_i16_lossy()) == "-12259i16" + assert str(addr.to_i32_lossy()) == "2078199837i32" + assert str(addr.to_i64_lossy()) == "-2882231367765143523i64" + assert str(addr.to_i128_lossy()) == "34922309281260474190457069241198628893i128" # --------------------------------------------------------------------------- @@ -212,30 +227,44 @@ def test_different_runs_different_ciphertext(self): assert ct1 != ct2 def test_interop_with_python_encryptor(self): - """Python Encryptor and Rust PrivateKeyCiphertext must interoperate. + """Cross-scheme decryption must work in BOTH directions. - The Python Encryptor uses symmetric field-based encryption (Poseidon2 - over the seed field element). PrivateKeyCiphertext uses the same - underlying scheme, so both sides should be able to round-trip through - the other. + The Python Encryptor and Rust PrivateKeyCiphertext implement the same + underlying scheme (Poseidon2 hash-based symmetric encryption over the + seed field element). True scheme identity means each side can decrypt + what the other encrypted — string-format compatibility alone is not + sufficient. Equality of the recovered PrivateKey is the only proof. """ - from aleo.mainnet import PrivateKeyCiphertext + from aleo.mainnet import Ciphertext, PrivateKeyCiphertext + SECRET = CIPHERTEXT_SECRET pk = PrivateKey.random() - # Encrypt in Python, decrypt in Rust. - ct_py = Encryptor.encrypt_private_key_with_secret(pk, CIPHERTEXT_SECRET) - ct_rust = PrivateKeyCiphertext.from_string(str(ct_py)) - pk_rust = ct_rust.decrypt_to_private_key(CIPHERTEXT_SECRET) - assert pk == pk_rust - - # Encrypt in Rust, decrypt in Python. - # The Python Encryptor uses aleo.Ciphertext, so convert via string. - from aleo.mainnet import Ciphertext - ct_rust2 = PrivateKeyCiphertext.encrypt_private_key(pk, CIPHERTEXT_SECRET) - ct_as_ciphertext = Ciphertext.from_string(str(ct_rust2)) - pk_py = Encryptor.decrypt_private_key_with_secret(ct_as_ciphertext, CIPHERTEXT_SECRET) - assert pk == pk_py + # --- Direction A: encrypt with pure-Python Encryptor, decrypt with Rust --- + # The Python Encryptor derives its symmetric key independently from the + # seed field via Network.hash_psd2. Parsing the ciphertext string into + # PrivateKeyCiphertext and calling decrypt_to_private_key with the same + # secret must recover the identical original key. + ct_py = Encryptor.encrypt_private_key_with_secret(pk, SECRET) + ct_rust_parsed = PrivateKeyCiphertext.from_string(str(ct_py)) + pk_from_rust_decrypt = ct_rust_parsed.decrypt_to_private_key(SECRET) + assert pk_from_rust_decrypt == pk, ( + "Direction A failed: Rust could not decrypt a Python-encrypted ciphertext. " + "This means the two implementations use different encryption schemes." + ) + + # --- Direction B: encrypt with Rust PrivateKeyCiphertext, decrypt with Python --- + # The Rust PrivateKeyCiphertext.encrypt_private_key derives its symmetric + # key independently. Converting to a Ciphertext via string round-trip and + # decrypting with Encryptor.decrypt_private_key_with_secret must recover + # the identical original key. + ct_rust = PrivateKeyCiphertext.encrypt_private_key(pk, SECRET) + ct_py_parsed = Ciphertext.from_string(str(ct_rust)) + pk_from_py_decrypt = Encryptor.decrypt_private_key_with_secret(ct_py_parsed, SECRET) + assert pk_from_py_decrypt == pk, ( + "Direction B failed: Python could not decrypt a Rust-encrypted ciphertext. " + "This means the two implementations use different encryption schemes." + ) # --------------------------------------------------------------------------- From f57645cea3ca8b470105635477426a33764924d9 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 11:28:56 -0400 Subject: [PATCH 39/72] feat: full wasm parity for Transaction, Transition, Execution, Authorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transaction: id/transaction_type/fee getters, is_deploy/execute/fee, records/owned_records/find_record, contains_{commitment,serial_number}, execution/transitions/deployed_program/verifying_keys/summary. Transition: tpk/tcm/scm getters, tvk(view_key), records/owned_records/ find_record, contains_{commitment,serial_number}, inputs()/outputs() as Python dicts (mirroring wasm input/output_to_js_value shapes incl. dynamic record/future variants), decrypt_transition(tvk) ported from wasm. Execution: global_state_root getter, proof(), transitions(). Authorization: to_execution_id, transitions, function_name, is_fee_private/ is_fee_public/is_split, __len__, replicate, insert_transition (unfrozen — snarkvm Authorization has interior mutability). Adds Input/Output/Future/Argument native type aliases to types.rs, updates both stub files (pyright clean), and vendors wasm transition KATs plus a local execution fixture in test_chain_data.py (35 new tests; 276 total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 53 +++- sdk/python/aleo/_aleolib_mainnet.pyi | 53 ++++ sdk/python/tests/test_chain_data.py | 348 ++++++++++++++++++++++++ sdk/python/tests/vectors/execution.json | 1 + sdk/src/programs/authorization.rs | 62 ++++- sdk/src/programs/execution.rs | 20 +- sdk/src/programs/transaction.rs | 140 +++++++++- sdk/src/programs/transition.rs | 338 ++++++++++++++++++++++- sdk/src/types.rs | 15 +- 9 files changed, 1017 insertions(+), 13 deletions(-) create mode 100644 sdk/python/tests/test_chain_data.py create mode 100644 sdk/python/tests/vectors/execution.json diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index 42dea50..e4bf055 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import List, Mapping, Tuple +from typing import Any, Dict, List, Mapping, Tuple class Account: @@ -61,6 +61,15 @@ class Authorization: @staticmethod def from_bytes(bytes: bytes) -> Authorization: ... def bytes(self) -> bytes: ... + def to_execution_id(self) -> Field: ... + def transitions(self) -> List[Transition]: ... + def function_name(self) -> str: ... + def is_fee_private(self) -> bool: ... + def is_fee_public(self) -> bool: ... + def is_split(self) -> bool: ... + def __len__(self) -> int: ... + def replicate(self) -> Authorization: ... + def insert_transition(self, transition: Transition) -> None: ... class BHP256: @@ -137,6 +146,10 @@ class Encryptor: class Execution: @property def execution_id(self) -> Field: ... + @property + def global_state_root(self) -> str: ... + def proof(self) -> str | None: ... + def transitions(self) -> List[Transition]: ... @staticmethod def from_json(json: str) -> Execution: ... def to_json(self) -> str: ... @@ -627,6 +640,29 @@ class Transaction: @staticmethod def from_bytes(bytes: bytes) -> Transaction: ... def bytes(self) -> bytes: ... + @property + def id(self) -> str: ... + @property + def transaction_type(self) -> str: ... + def is_deploy(self) -> bool: ... + def is_execute(self) -> bool: ... + def is_fee(self) -> bool: ... + @property + def base_fee_amount(self) -> int: ... + @property + def fee_amount(self) -> int: ... + @property + def priority_fee_amount(self) -> int: ... + def records(self) -> List[Tuple[Field, RecordCiphertext]]: ... + def owned_records(self, view_key: ViewKey) -> List[RecordPlaintext]: ... + def find_record(self, commitment: Field) -> RecordCiphertext | None: ... + def contains_serial_number(self, serial_number: Field) -> bool: ... + def contains_commitment(self, commitment: Field) -> bool: ... + def execution(self) -> Execution | None: ... + def transitions(self) -> List[Transition]: ... + def deployed_program(self) -> Program | None: ... + def verifying_keys(self) -> List[Dict[str, Any]]: ... + def summary(self) -> Dict[str, Any]: ... class Transition: @@ -647,6 +683,21 @@ class Transition: @staticmethod def from_bytes(bytes: bytes) -> Transition: ... def bytes(self) -> bytes: ... + @property + def tpk(self) -> Group: ... + @property + def tcm(self) -> Field: ... + @property + def scm(self) -> Field: ... + def tvk(self, view_key: ViewKey) -> Field: ... + def records(self) -> List[Tuple[Field, RecordCiphertext]]: ... + def owned_records(self, view_key: ViewKey) -> List[RecordPlaintext]: ... + def find_record(self, commitment: Field) -> RecordCiphertext | None: ... + def contains_commitment(self, commitment: Field) -> bool: ... + def contains_serial_number(self, serial_number: Field) -> bool: ... + def inputs(self) -> List[Dict[str, Any]]: ... + def outputs(self) -> List[Dict[str, Any]]: ... + def decrypt_transition(self, tvk: Field) -> Transition: ... class U8: diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index 69d46c1..bf96811 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + class Account: @staticmethod def random() -> Account: ... @@ -59,6 +61,15 @@ class Authorization: @staticmethod def from_bytes(bytes: bytes) -> Authorization: ... def bytes(self) -> bytes: ... + def to_execution_id(self) -> Field: ... + def transitions(self) -> list[Transition]: ... + def function_name(self) -> str: ... + def is_fee_private(self) -> bool: ... + def is_fee_public(self) -> bool: ... + def is_split(self) -> bool: ... + def __len__(self) -> int: ... + def replicate(self) -> Authorization: ... + def insert_transition(self, transition: Transition) -> None: ... class BHP256: @@ -128,6 +139,10 @@ class ComputeKey: class Execution: @property def execution_id(self) -> Field: ... + @property + def global_state_root(self) -> str: ... + def proof(self) -> str | None: ... + def transitions(self) -> list[Transition]: ... @staticmethod def from_json(json: str) -> Execution: ... def to_json(self) -> str: ... @@ -614,6 +629,29 @@ class Transaction: @staticmethod def from_bytes(bytes: bytes) -> Transaction: ... def bytes(self) -> bytes: ... + @property + def id(self) -> str: ... + @property + def transaction_type(self) -> str: ... + def is_deploy(self) -> bool: ... + def is_execute(self) -> bool: ... + def is_fee(self) -> bool: ... + @property + def base_fee_amount(self) -> int: ... + @property + def fee_amount(self) -> int: ... + @property + def priority_fee_amount(self) -> int: ... + def records(self) -> list[tuple[Field, RecordCiphertext]]: ... + def owned_records(self, view_key: ViewKey) -> list[RecordPlaintext]: ... + def find_record(self, commitment: Field) -> RecordCiphertext | None: ... + def contains_serial_number(self, serial_number: Field) -> bool: ... + def contains_commitment(self, commitment: Field) -> bool: ... + def execution(self) -> Execution | None: ... + def transitions(self) -> list[Transition]: ... + def deployed_program(self) -> Program | None: ... + def verifying_keys(self) -> list[dict[str, Any]]: ... + def summary(self) -> dict[str, Any]: ... class Transition: @@ -634,6 +672,21 @@ class Transition: @staticmethod def from_bytes(bytes: bytes) -> Transition: ... def bytes(self) -> bytes: ... + @property + def tpk(self) -> Group: ... + @property + def tcm(self) -> Field: ... + @property + def scm(self) -> Field: ... + def tvk(self, view_key: ViewKey) -> Field: ... + def records(self) -> list[tuple[Field, RecordCiphertext]]: ... + def owned_records(self, view_key: ViewKey) -> list[RecordPlaintext]: ... + def find_record(self, commitment: Field) -> RecordCiphertext | None: ... + def contains_commitment(self, commitment: Field) -> bool: ... + def contains_serial_number(self, serial_number: Field) -> bool: ... + def inputs(self) -> list[dict[str, Any]]: ... + def outputs(self) -> list[dict[str, Any]]: ... + def decrypt_transition(self, tvk: Field) -> Transition: ... class U8: diff --git a/sdk/python/tests/test_chain_data.py b/sdk/python/tests/test_chain_data.py new file mode 100644 index 0000000..0fd1c1b --- /dev/null +++ b/sdk/python/tests/test_chain_data.py @@ -0,0 +1,348 @@ +"""Tests for chain-data introspection: Transition, Execution, Authorization, Transaction. + +Transition fixtures and KATs are vendored verbatim from the wasm SDK test suite +(wasm/src/ledger/transition.rs). The execution fixture (vectors/execution.json) +is a locally proven mainnet credits.aleo/transfer_public execution. + +All tests are offline. The Authorization tests use the offline authorize +pattern (Process.authorize needs no network; first-ever run downloads a small +~3 MB SRS chunk to ~/.aleo). +""" + +import json + +import pytest + +from aleo.mainnet import ( + Authorization, + Execution, + Field, + Identifier, + PrivateKey, + Process, + ProgramID, + Transaction, + Transition, + Value, +) +from conftest import VECTORS + +# --------------------------------------------------------------------------- +# Vendored KATs from wasm/src/ledger/transition.rs +# --------------------------------------------------------------------------- + +INPUT_RECORD_SERIAL_NUMBER = "4569194627311410524427044648350523511369013276760031398859310110870190258038field" +INPUT_RECORD_TAG = "4584393733726099907383249165298083023636530416018938077800083356406243497342field" +OUTPUT_CHECKSUM = "17461704767783030875142836237730678349755524657182224909428747180538982740field" +OUTPUT_RECORD = "record1qyqspwnlv6gfxx05yj7aw7z2dl44gyh06jrvgf42jux0dep33cy7jlsvqsrxzmt0w4h8ggcqqgqsqwdwr889h9fhnyclazs8yt26t6r5ua4qk7yksj7p40rz9846mzgrpp6x76m9de0kjezrqqpqyq9sj8x3qdmz6nal4j470a0wwcray54lffe3ya5u2zlpeq45lg4up3na8gul0vgrn3eced6dka4ax2ja85xzds4pmqf8csrn8ku5cv3qz8m90p6x2unwv9k97ct4w35x7unf0fshg6t0de0hyet3w45hyetyyvqqyqgq8djhghnte2w86qsdjaumy4zcux2fxszm3ej2956af8cpl2w95g9pqct4w35x7unf0fjkghm4de6xjmprqqpqzqxd6c782j0ny65ed2ckzp3vlx7cv8drslasq8kqpdzmjeyzal38qemw38x0axnz5t53fj6ttavh8l4jlfjdryc6mesd4w6uvpmzfsqqjvtu0xd" +OUTPUT_RECORD_COMMITMENT = "3771264214823666953346974490700157125043441681812666085949968314967709800215field" +TRANSITION = r'{"id":"au1naeu56spz0x0zct003sa8qgpzndy6nxj8rrcm7n0fehy9llcl5yscflt0k","program":"token_registry.aleo","function":"burn_private","inputs":[{"type":"record","id":"4569194627311410524427044648350523511369013276760031398859310110870190258038field","tag":"4584393733726099907383249165298083023636530416018938077800083356406243497342field"},{"type":"public","id":"4155661860779318196369465902681808025430867777096367712868886959018716227815field","value":"2853086u128"}],"outputs":[{"type":"record","id":"3771264214823666953346974490700157125043441681812666085949968314967709800215field","checksum":"17461704767783030875142836237730678349755524657182224909428747180538982740field","value":"record1qyqspwnlv6gfxx05yj7aw7z2dl44gyh06jrvgf42jux0dep33cy7jlsvqsrxzmt0w4h8ggcqqgqsqwdwr889h9fhnyclazs8yt26t6r5ua4qk7yksj7p40rz9846mzgrpp6x76m9de0kjezrqqpqyq9sj8x3qdmz6nal4j470a0wwcray54lffe3ya5u2zlpeq45lg4up3na8gul0vgrn3eced6dka4ax2ja85xzds4pmqf8csrn8ku5cv3qz8m90p6x2unwv9k97ct4w35x7unf0fshg6t0de0hyet3w45hyetyyvqqyqgq8djhghnte2w86qsdjaumy4zcux2fxszm3ej2956af8cpl2w95g9pqct4w35x7unf0fjkghm4de6xjmprqqpqzqxd6c782j0ny65ed2ckzp3vlx7cv8drslasq8kqpdzmjeyzal38qemw38x0axnz5t53fj6ttavh8l4jlfjdryc6mesd4w6uvpmzfsqqjvtu0xd","sender_ciphertext":null},{"type":"future","id":"2177527202823505610844479366424698260670813913152550670302738921219693374616field","value":"{\n program_id: token_registry.aleo,\n function_name: burn_private,\n arguments: [\n 3443843282313283355522573239085696902919850365217539366784739393210722344986field,\n 2853086u128,\n aleo1tjkv7vquk6yldxz53ecwsy5csnun43rfaknpkjc97v5223dlnyxsglv7nm,\n 5783861720504029593520331872442756678068735468923730684279741068753131773333field\n ]\n}"}],"tpk":"8426225807947287980879824833030089440060785195861154519084544916641544071836group","tcm":"3226339871444496417979841037237975848011574524309845233165930705339306709897field","scm":"6845182532650964173356391969005331370591444046632036068754797772530920467754field"}' +TEST_PRIVATE_KEY = "APrivateKey1zkp6rE5FSWGD3jxrsAT64aZutFs3w6xvF8uQzGZKJEKsN8j" +EXPECTED_TVK = "4386935145534748320784836619728244316439880324135120862336274251207085504468field" + +# Mainnet decryption fixtures (hello_hello.aleo/main), signer key below. +TRANSITION_MAINNET = r'{"id":"au1mguuz0dh20f78802m4z0py7n08xhl0pz60llzck63mhl8pc8l5xqxpwgtn","program":"hello_hello.aleo","function":"main","inputs":[{"type":"public","id":"6393584049543470937057043098611271993206122889317039351966319038535020834557field","value": "1u32"},{"type":"private","id":"8207446256045172951742235001162005156507562935942883128759030124682934277495field","value":"ciphertext1qyqqgz9qnupeld9vr4vuwp6yrpmhgtkvmgag5m7mmrruw0r6je666qgqdswk3"}],"outputs":[{"type":"private","id":"127469473292952941321346770257126666363371158501875622169294663492714835110field","value":"ciphertext1qyqyapkjuxm9dcslgyjf7hkr2k3dek500z40gjspnwvll0uawj23vzgggc405"}],"tpk":"7647553513996966044119163122930125808381703910407273818947266861843062002251group","tcm":"4479413938380109857414238205380483440836495997450846894155088299187217672609field","scm":"6461007226176477784737642021400489186736987671609840640950580467598882134642field"}' +TRANSITION_MAINNET_DECRYPTED = r'{"id":"au1jl2ur42sj7hwe4r0alv6gnklqxj0fszrvu3q82gjcls5x6q9pyzqdgmu2k","program":"hello_hello.aleo","function":"main","inputs":[{"type":"public","id":"6393584049543470937057043098611271993206122889317039351966319038535020834557field","value":"1u32"},{"type":"public","id":"8207446256045172951742235001162005156507562935942883128759030124682934277495field","value":"2u32"}],"outputs":[{"type":"public","id":"127469473292952941321346770257126666363371158501875622169294663492714835110field","value":"3u32"}],"tpk":"7647553513996966044119163122930125808381703910407273818947266861843062002251group","tcm":"4479413938380109857414238205380483440836495997450846894155088299187217672609field","scm":"6461007226176477784737642021400489186736987671609840640950580467598882134642field"}' +DECRYPTION_PRIVATE_KEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" + + +@pytest.fixture(scope="module") +def transition() -> Transition: + return Transition.from_json(TRANSITION) + + +@pytest.fixture(scope="module") +def execution() -> Execution: + return Execution.from_json((VECTORS / "execution.json").read_text()) + + +@pytest.fixture(scope="module") +def process() -> Process: + return Process.load() + + +def _authorize_transfer_public(process: Process, private_key: PrivateKey) -> Authorization: + return process.authorize( + private_key, + ProgramID.from_string("credits.aleo"), + Identifier.from_string("transfer_public"), + [Value.parse(str(private_key.address)), Value.parse("10u64")], + ) + + +@pytest.fixture(scope="module") +def authorization(process) -> Authorization: + return _authorize_transfer_public(process, PrivateKey.random()) + + +# --------------------------------------------------------------------------- +# Transition +# --------------------------------------------------------------------------- + + +class TestTransition: + def test_from_json_roundtrip(self, transition): + # serde drops null optional fields (e.g. "sender_ciphertext": null), + # so compare parsed structures with nulls stripped, plus a bytes-level + # roundtrip which is exact. + def strip_nulls(obj): + if isinstance(obj, dict): + return {k: strip_nulls(v) for k, v in obj.items() if v is not None} + if isinstance(obj, list): + return [strip_nulls(x) for x in obj] + return obj + + assert json.loads(transition.to_json()) == strip_nulls(json.loads(TRANSITION)) + assert Transition.from_json(transition.to_json()).bytes() == transition.bytes() + + def test_tpk_getter(self, transition): + assert ( + str(transition.tpk) + == "8426225807947287980879824833030089440060785195861154519084544916641544071836group" + ) + + def test_tcm_getter(self, transition): + assert ( + str(transition.tcm) + == "3226339871444496417979841037237975848011574524309845233165930705339306709897field" + ) + + def test_scm_getter(self, transition): + assert ( + str(transition.scm) + == "6845182532650964173356391969005331370591444046632036068754797772530920467754field" + ) + + def test_tvk(self, transition): + pk = PrivateKey.from_string(TEST_PRIVATE_KEY) + assert str(transition.tvk(pk.view_key)) == EXPECTED_TVK + + def test_records(self, transition): + records = transition.records() + assert len(records) == 1 + commitment, record_ciphertext = records[0] + assert str(commitment) == OUTPUT_RECORD_COMMITMENT + assert str(record_ciphertext) == OUTPUT_RECORD + + def test_find_record(self, transition): + commitment = Field.from_string(OUTPUT_RECORD_COMMITMENT) + rc = transition.find_record(commitment) + assert rc is not None + assert str(rc) == OUTPUT_RECORD + assert transition.find_record(Field.random()) is None + + def test_contains_commitment(self, transition): + assert transition.contains_commitment(Field.from_string(OUTPUT_RECORD_COMMITMENT)) + assert not transition.contains_commitment(Field.random()) + + def test_contains_serial_number(self, transition): + assert transition.contains_serial_number(Field.from_string(INPUT_RECORD_SERIAL_NUMBER)) + assert not transition.contains_serial_number(Field.random()) + + def test_owned_records_random_vk_finds_none(self, transition): + vk = PrivateKey.random().view_key + assert len(transition.owned_records(vk)) == 0 + + def test_inputs_structure(self, transition): + inputs = transition.inputs() + assert len(inputs) == 2 + assert inputs[0]["type"] == "record" + assert inputs[0]["id"] == INPUT_RECORD_SERIAL_NUMBER + assert inputs[0]["tag"] == INPUT_RECORD_TAG + assert inputs[1]["type"] == "public" + assert ( + inputs[1]["id"] + == "4155661860779318196369465902681808025430867777096367712868886959018716227815field" + ) + assert inputs[1]["value"] == "2853086u128" + + def test_outputs_structure(self, transition): + outputs = transition.outputs() + assert len(outputs) == 2 + + assert outputs[0]["type"] == "record" + assert outputs[0]["id"] == OUTPUT_RECORD_COMMITMENT + assert outputs[0]["checksum"] == OUTPUT_CHECKSUM + assert outputs[0]["value"] == OUTPUT_RECORD + assert outputs[0]["sender_ciphertext"] is None + + assert outputs[1]["type"] == "future" + assert outputs[1]["program"] == "token_registry.aleo" + assert outputs[1]["function"] == "burn_private" + arguments = outputs[1]["arguments"] + assert len(arguments) == 4 + assert arguments[0] == ( + "3443843282313283355522573239085696902919850365217539366784739393210722344986field" + ) + assert arguments[1] == "2853086u128" + assert arguments[2] == "aleo1tjkv7vquk6yldxz53ecwsy5csnun43rfaknpkjc97v5223dlnyxsglv7nm" + assert arguments[3] == ( + "5783861720504029593520331872442756678068735468923730684279741068753131773333field" + ) + + def test_decrypt_transition(self): + pk = PrivateKey.from_string(DECRYPTION_PRIVATE_KEY) + t = Transition.from_json(TRANSITION_MAINNET) + tvk = t.tvk(pk.view_key) + decrypted = t.decrypt_transition(tvk) + assert json.loads(decrypted.to_json()) == json.loads(TRANSITION_MAINNET_DECRYPTED) + + def test_decrypt_transition_invalid_tvk(self): + # A tvk derived from the wrong view key must fail decryption. + wrong_pk = PrivateKey.from_string(TEST_PRIVATE_KEY) + t = Transition.from_json(TRANSITION_MAINNET) + invalid_tvk = t.tvk(wrong_pk.view_key) + with pytest.raises(RuntimeError): + t.decrypt_transition(invalid_tvk) + + +# --------------------------------------------------------------------------- +# Execution +# --------------------------------------------------------------------------- + + +class TestExecution: + def test_transitions(self, execution): + transitions = execution.transitions() + assert len(transitions) == 1 + assert str(transitions[0].program_id) == "credits.aleo" + assert str(transitions[0].function_name) == "transfer_public" + + def test_global_state_root(self, execution): + fixture = json.loads((VECTORS / "execution.json").read_text()) + assert execution.global_state_root == fixture["global_state_root"] + assert execution.global_state_root.startswith("sr1") + + def test_proof(self, execution): + fixture = json.loads((VECTORS / "execution.json").read_text()) + proof = execution.proof() + assert proof is not None + assert proof == fixture["proof"] + assert proof.startswith("proof1") + + def test_execution_id(self, execution): + assert str(execution.execution_id).endswith("field") + + +# --------------------------------------------------------------------------- +# Authorization +# --------------------------------------------------------------------------- + + +class TestAuthorization: + def test_to_execution_id(self, authorization): + eid = authorization.to_execution_id() + assert isinstance(eid, Field) + assert str(eid).endswith("field") + # Deterministic for the same authorization. + assert str(authorization.to_execution_id()) == str(eid) + + def test_transitions(self, authorization): + transitions = authorization.transitions() + assert len(transitions) == 1 + assert str(transitions[0].program_id) == "credits.aleo" + assert str(transitions[0].function_name) == "transfer_public" + + def test_function_name(self, authorization): + assert authorization.function_name() == "transfer_public" + + def test_is_fee_flags(self, authorization, process): + assert authorization.is_fee_private() is False + assert authorization.is_fee_public() is False + assert authorization.is_split() is False + + # A fee_public authorization must report is_fee_public. + fee_auth = process.authorize_fee_public( + PrivateKey.random(), 1_000_000, 0, Field.random() + ) + assert fee_auth.is_fee_public() is True + assert fee_auth.is_fee_private() is False + assert fee_auth.is_split() is False + assert fee_auth.function_name() == "fee_public" + + def test_len(self, authorization): + assert len(authorization) == 1 + + def test_replicate(self, authorization): + replica = authorization.replicate() + assert replica.to_json() == authorization.to_json() + assert str(replica.to_execution_id()) == str(authorization.to_execution_id()) + + def test_insert_transition_duplicate_rejected(self, authorization): + existing = authorization.transitions()[0] + with pytest.raises(RuntimeError): + authorization.insert_transition(existing) + + def test_insert_transition(self, process): + auth = _authorize_transfer_public(process, PrivateKey.random()) + other = _authorize_transfer_public(process, PrivateKey.random()) + foreign_transition = other.transitions()[0] + assert auth.insert_transition(foreign_transition) is None + assert len(auth.transitions()) == 2 + + def test_json_roundtrip(self, authorization): + roundtripped = Authorization.from_json(authorization.to_json()) + assert roundtripped.to_json() == authorization.to_json() + assert len(roundtripped) == len(authorization) + + +# --------------------------------------------------------------------------- +# Transaction (execute-type, built offline from the execution fixture) +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def transaction(execution) -> Transaction: + return Transaction.from_execution(execution, None) + + +class TestTransaction: + def test_id_and_type(self, transaction): + assert transaction.id.startswith("at1") + assert transaction.transaction_type == "execute" + assert transaction.is_execute() is True + assert transaction.is_deploy() is False + assert transaction.is_fee() is False + + def test_fee_amounts(self, transaction): + # No fee was attached, so all fee amounts are zero. + assert transaction.fee_amount == 0 + assert transaction.base_fee_amount == 0 + assert transaction.priority_fee_amount == 0 + + def test_execution_and_transitions(self, transaction, execution): + embedded = transaction.execution() + assert embedded is not None + assert str(embedded.execution_id) == str(execution.execution_id) + transitions = transaction.transitions() + assert len(transitions) == 1 + assert str(transitions[0].program_id) == "credits.aleo" + + def test_records_empty_for_public_transfer(self, transaction): + assert transaction.records() == [] + assert len(transaction.owned_records(PrivateKey.random().view_key)) == 0 + + def test_find_and_contains(self, transaction): + random_field = Field.random() + assert transaction.find_record(random_field) is None + assert transaction.contains_commitment(random_field) is False + assert transaction.contains_serial_number(random_field) is False + + def test_deploy_accessors_on_execute(self, transaction): + assert transaction.deployed_program() is None + assert transaction.verifying_keys() == [] + + def test_summary(self, transaction): + summary = transaction.summary() + assert summary["id"] == transaction.id + assert summary["type"] == "execute" + assert summary["fee_amount"] == 0 + assert summary["base_fee"] == 0 + assert summary["priority_fee"] == 0 + + def test_json_roundtrip(self, transaction): + parsed = json.loads(transaction.to_json()) + assert parsed["type"] == "execute" + roundtripped = Transaction.from_json(transaction.to_json()) + assert roundtripped == transaction + assert roundtripped.id == transaction.id diff --git a/sdk/python/tests/vectors/execution.json b/sdk/python/tests/vectors/execution.json new file mode 100644 index 0000000..740cf1c --- /dev/null +++ b/sdk/python/tests/vectors/execution.json @@ -0,0 +1 @@ +{"transitions":[{"id":"au13m0d8064tlut5mamzy9n6lzwty8v20uj2ahmx927jwh4z49yxgpqmnq8km","program":"credits.aleo","function":"transfer_public","inputs":[{"type":"public","id":"5232202457480057385247290064991301393364283059757646033184061302414082051569field","value":"aleo1p94yjtsjs486judrrp2whhfmp4uwjw05s3sspqwh2lqzu2t7dggsrjs8q9"},{"type":"public","id":"6039864465392675601281968533091094606395666092181252388654350752976964492906field","value":"10u64"}],"outputs":[{"type":"future","id":"2849096635839886570053898295837297455429916942196425558286771624069954785662field","value":"{\n program_id: credits.aleo,\n function_name: transfer_public,\n arguments: [\n aleo1p94yjtsjs486judrrp2whhfmp4uwjw05s3sspqwh2lqzu2t7dggsrjs8q9,\n aleo1p94yjtsjs486judrrp2whhfmp4uwjw05s3sspqwh2lqzu2t7dggsrjs8q9,\n 10u64\n ]\n}"}],"tpk":"5442292273102810617220592865289542482821203063436071613143952577380278365085group","tcm":"6388570576522246614267351508865269556018693105793398695736873225018586693288field","scm":"5752126838598109818001248660714347493073097780760900839776877351662662490722field"}],"global_state_root":"sr167q53pe3cwuvz8tgu3tl7r74hd97xj369mmav44lqr4u2r22vggq27y0m5","proof":"proof1qyqsqqqqqqqqqqqpqqqqqqqqqqqd2vjvn2pn9wqwzmtvndq4ffn0d9a3nmhp6nj3anklz05gxagtht6kd4k8qeep4qyddcgpc5dn4fuqq9e7aywdu78r3vt7dlfgzv2k6nz6cxcdmamlkasxx3av6067ncq8nje2hdgvnxfxjxjcdwzje0f70q8gttdeqeltd3ujesqdyf5v2g4shg0wz4jk5wrzcsefz80mjwvawsk49j5nfc34v7axt2u599df4qqvqlrzx7zalktpmtwnwdtuu7nf99t0lecatyfu9gl8uvzy2844djuxtj76a5xnkur88kgmkkenw7cp7peu64vejkvu6jtalde4346l5dxn25ze5hyxn88mfd8skp2q2edgxf8r6hekj59zsdasjq05e6wszd9dkpulymvg6n0jcepu5vtgk02zgv098a0kpq03879nlhgrahtc0mjd397athsapt0u25hmmhzlsqdrczw4ea38yprnaapg6et4v6n39l2wanc46l48kacfce03ap5wngw3vkjh976eryaz6rcval39tqzge9zfrpvuetsjmn37y2mcwv8hznnevkg06q8cqkn3z73dv7jn4sgedl8n6wd8pd9pfuyl766742q07ttztnnpt6zl44dlyk4yug78l344mhvru50r3yapn63ctjnsnxlgnrcl5ypgp0357r6dx7hq9xcp5gtrmehkum0m8eg5uadwzllyy0slaj0nw4qev2wmjv4ks5fuyvg4r29equ2hldqtlh9adx6hegee93m2c5j7w7g8rlyhrtemj3082qlytvadh6enq64cyuajchl5gk3drsdzgwg4x9693fg7h53jxxl4qcxvmracweheyawyeaagynh5w3kxzlnr993hmlcrk66a3lg4uu63q3klajwzz44w06mtc5jp9wstp9r8dmten75dcmrfcxpqurezacqxckfhakgcmq0pspeud85htpus5vkantwyr9dvn2u3fyaz86qnr5y3ecj5lvpj3s8nccqw8yfn3l3k9k7l3lrkycp7pu0x27nxf4skcyy8th84nkv8agjhlcelvf432jd6x4h667wuycl7a9qnt20c972zqwvjz2u0fxysjcmaes693e5lv2pmcplycs9ap7sq49f32ch0ejhsf64cuqjutk8ykd7ly9xmuhfhfhcunc3e80cnvjnc52d62p62x0szqvqqqqqqqqqqpc98qr20xuwuwsu05p07c2k09t5v8ftxqhzw0x47wjec20qf0tu0h45vm7tq36knj3mq76wha2v0sqqvpufwxy63gznjx3eacdymkcphpjxh89f6fdqg5l8rh3yny35fxndzka3za6utrywptqvnahyrgkqpq968phx99nm4ywp7ssvzqrrnc073a52wm89wkr4zgkzf5k4wcedsed6myejv5t6lw04fmqzrr3j5gp86p4kwujxrkj2rks38vg4v55ejm94fm9cy2fm9zctg2qwr35whqqqqqytwa5"} \ No newline at end of file diff --git a/sdk/src/programs/authorization.rs b/sdk/src/programs/authorization.rs index ce3f196..8a298e6 100644 --- a/sdk/src/programs/authorization.rs +++ b/sdk/src/programs/authorization.rs @@ -14,13 +14,17 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::types::AuthorizationNative; +use crate::{ + programs::Transition, + types::{AuthorizationNative, TransitionNative}, + Field, +}; use pyo3::prelude::*; use snarkvm::prelude::{FromBytes, ToBytes}; /// The Aleo authorization type. -#[pyclass(frozen)] +#[pyclass] #[derive(Clone)] pub struct Authorization(AuthorizationNative); @@ -52,6 +56,60 @@ impl Authorization { fn __str__(&self) -> anyhow::Result<String> { self.to_json() } + + // ---- new methods ---- + + /// Returns the execution ID for this authorization. + fn to_execution_id(&self) -> anyhow::Result<Field> { + self.0.to_execution_id().map(Into::into) + } + + /// Returns the list of transitions in this authorization. + fn transitions(&self) -> Vec<Transition> { + self.0 + .transitions() + .into_iter() + .map(|(_, t)| Transition::from(t)) + .collect() + } + + /// Returns the function name of the first request. + fn function_name(&self) -> anyhow::Result<String> { + Ok(self.0.get(0)?.function_name().to_string()) + } + + /// Returns true if the authorization is for `credits.aleo/fee_private`. + fn is_fee_private(&self) -> bool { + self.0.is_fee_private() + } + + /// Returns true if the authorization is for `credits.aleo/fee_public`. + fn is_fee_public(&self) -> bool { + self.0.is_fee_public() + } + + /// Returns true if the authorization is for `credits.aleo/split`. + fn is_split(&self) -> bool { + self.0.is_split() + } + + /// Returns the number of requests in the authorization. + fn __len__(&self) -> usize { + self.0.len() + } + + /// Returns a new and independent replica of the authorization. + fn replicate(&self) -> Authorization { + Authorization(self.0.replicate()) + } + + /// Inserts a transition into the authorization. + fn insert_transition(&self, transition: &Transition) -> anyhow::Result<()> { + let native = <TransitionNative as From<&Transition>>::from(transition); + self.0 + .insert_transition(native) + .map_err(|e| anyhow::anyhow!("{e}")) + } } impl From<AuthorizationNative> for Authorization { diff --git a/sdk/src/programs/execution.rs b/sdk/src/programs/execution.rs index 4877fe9..e494793 100644 --- a/sdk/src/programs/execution.rs +++ b/sdk/src/programs/execution.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::{types::ExecutionNative, Field}; +use crate::{programs::Transition, types::ExecutionNative, Field}; use pyo3::prelude::*; use snarkvm::prelude::{FromBytes, ToBytes}; @@ -60,6 +60,24 @@ impl Execution { fn __str__(&self) -> anyhow::Result<String> { self.to_json() } + + // ---- new methods ---- + + /// Returns the global state root as a string. + #[getter] + fn global_state_root(&self) -> String { + self.0.global_state_root().to_string() + } + + /// Returns the proof as a string, or None if not present. + fn proof(&self) -> Option<String> { + self.0.proof().map(|p| p.to_string()) + } + + /// Returns the list of transitions in this execution. + fn transitions(&self) -> Vec<Transition> { + self.0.transitions().map(Transition::from).collect() + } } impl Deref for Execution { diff --git a/sdk/src/programs/transaction.rs b/sdk/src/programs/transaction.rs index d23e4c3..11ffa65 100644 --- a/sdk/src/programs/transaction.rs +++ b/sdk/src/programs/transaction.rs @@ -15,11 +15,15 @@ // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. use crate::{ - programs::{Execution, Fee}, + programs::{Execution, Fee, Program, Transition}, types::TransactionNative, + Field, RecordCiphertext, RecordPlaintext, ViewKey, }; -use pyo3::prelude::*; +use pyo3::{ + prelude::*, + types::{PyDict, PyList, PyTuple}, +}; use snarkvm::prelude::{FromBytes, ToBytes}; use std::str::FromStr; @@ -68,6 +72,138 @@ impl Transaction { #[classattr] const __hash__: Option<PyObject> = None; + + // ---- new methods ---- + + /// Returns the transaction ID string. + #[getter] + fn id(&self) -> String { + self.0.id().to_string() + } + + /// Returns "deploy", "execute", or "fee". + #[getter] + fn transaction_type(&self) -> String { + match &self.0 { + TransactionNative::Deploy(..) => "deploy".to_string(), + TransactionNative::Execute(..) => "execute".to_string(), + TransactionNative::Fee(..) => "fee".to_string(), + } + } + + /// Returns true if the transaction is a deployment transaction. + fn is_deploy(&self) -> bool { + self.0.is_deploy() + } + + /// Returns true if the transaction is an execution transaction. + fn is_execute(&self) -> bool { + self.0.is_execute() + } + + /// Returns true if the transaction is a fee transaction. + fn is_fee(&self) -> bool { + self.0.is_fee() + } + + /// Returns the transaction's base fee amount. + #[getter] + fn base_fee_amount(&self) -> u64 { + self.0.base_fee_amount().map(|f| *f).unwrap_or(0) + } + + /// Returns the transaction's total fee amount. + #[getter] + fn fee_amount(&self) -> u64 { + self.0.fee_amount().map(|f| *f).unwrap_or(0) + } + + /// Returns the transaction's priority fee amount. + #[getter] + fn priority_fee_amount(&self) -> u64 { + self.0.priority_fee_amount().map(|f| *f).unwrap_or(0) + } + + /// Returns a list of (Field, RecordCiphertext) tuples for all records in the transaction. + fn records(&self, py: Python) -> PyObject { + let list = PyList::empty(py); + for (commitment, record) in self.0.records() { + let f: Field = (*commitment).into(); + let rc: RecordCiphertext = record.clone().into(); + let t = PyTuple::new(py, [f.into_py(py), rc.into_py(py)]); + list.append(t).unwrap(); + } + list.into_py(py) + } + + /// Returns list of RecordPlaintext owned by view_key. + fn owned_records(&self, view_key: &ViewKey) -> Vec<RecordPlaintext> { + self.0 + .records() + .filter_map(|(_, rc)| rc.decrypt(view_key).ok().map(RecordPlaintext::from)) + .collect() + } + + /// Returns the RecordCiphertext for a commitment, or None. + fn find_record(&self, commitment: &Field) -> Option<RecordCiphertext> { + self.0.find_record(commitment).map(|rc| rc.clone().into()) + } + + /// Returns true if the transaction contains the given serial number. + fn contains_serial_number(&self, serial_number: &Field) -> bool { + self.0.contains_serial_number(serial_number) + } + + /// Returns true if the transaction contains the given commitment. + fn contains_commitment(&self, commitment: &Field) -> bool { + self.0.contains_commitment(commitment) + } + + /// Returns the execution within the transaction (if present). + fn execution(&self) -> Option<Execution> { + self.0.execution().map(|e| Execution::from(e.clone())) + } + + /// Returns the list of transitions in the transaction. + fn transitions(&self) -> Vec<Transition> { + self.0.transitions().map(Transition::from).collect() + } + + /// Returns the program deployed within the transaction (if it is a deployment transaction). + fn deployed_program(&self) -> Option<Program> { + self.0 + .deployment() + .map(|d| Program::from(d.program().clone())) + } + + /// Returns a list of dicts with keys "program", "function", "verifying_key", "certificate". + fn verifying_keys(&self, py: Python) -> PyObject { + let list = PyList::empty(py); + if let Some(deployment) = self.0.deployment() { + for (fname, (vk, cert)) in deployment.verifying_keys() { + let d = PyDict::new(py); + d.set_item("program", deployment.program_id().to_string()) + .unwrap(); + d.set_item("function", fname.to_string()).unwrap(); + d.set_item("verifying_key", vk.to_string()).unwrap(); + d.set_item("certificate", cert.to_string()).unwrap(); + list.append(d).unwrap(); + } + } + list.into_py(py) + } + + /// Returns a Python dict summarizing the transaction (id, type, fee fields). + fn summary(&self, py: Python) -> PyObject { + let d = PyDict::new(py); + d.set_item("id", self.id()).unwrap(); + d.set_item("type", self.transaction_type()).unwrap(); + d.set_item("fee_amount", self.fee_amount()).unwrap(); + d.set_item("base_fee", self.base_fee_amount()).unwrap(); + d.set_item("priority_fee", self.priority_fee_amount()) + .unwrap(); + d.into_py(py) + } } impl From<TransactionNative> for Transaction { diff --git a/sdk/src/programs/transition.rs b/sdk/src/programs/transition.rs index ba7890a..7586926 100644 --- a/sdk/src/programs/transition.rs +++ b/sdk/src/programs/transition.rs @@ -16,11 +16,18 @@ use crate::{ programs::{Identifier, ProgramID}, - types::TransitionNative, + types::{ + ArgumentNative, CurrentNetwork, FieldNative, FutureNative, InputNative, OutputNative, + TransitionNative, U16Native, + }, + Field, Group, RecordCiphertext, RecordPlaintext, ViewKey, }; -use pyo3::prelude::*; -use snarkvm::prelude::{FromBytes, ToBytes}; +use pyo3::{ + prelude::*, + types::{PyDict, PyList, PyTuple}, +}; +use snarkvm::prelude::{compute_function_id, FromBytes, Network, ToBytes}; /// The Aleo transition type. #[pyclass(frozen)] @@ -97,6 +104,313 @@ impl Transition { fn __str__(&self) -> anyhow::Result<String> { self.to_json() } + + // ---- new methods ---- + + /// Returns the transition public key (tpk). + #[getter] + fn tpk(&self) -> Group { + Group::from(self.0.tpk()) + } + + /// Returns the transition commitment (tcm). + #[getter] + fn tcm(&self) -> Field { + (*self.0.tcm()).into() + } + + /// Returns the transition signer commitment (scm). + #[getter] + fn scm(&self) -> Field { + (*self.0.scm()).into() + } + + /// Returns the transition view key: `(tpk * view_key_scalar).to_x_coordinate()`. + fn tvk(&self, view_key: &ViewKey) -> Field { + (*self.0.tpk() * ***view_key).to_x_coordinate().into() + } + + /// Returns a list of (Field, RecordCiphertext) tuples for all records in this transition. + fn records(&self, py: Python) -> PyObject { + let list = PyList::empty(py); + for (commitment, record) in self.0.records() { + let f: Field = (*commitment).into(); + let rc: RecordCiphertext = record.clone().into(); + let t = PyTuple::new(py, [f.into_py(py), rc.into_py(py)]); + list.append(t).unwrap(); + } + list.into_py(py) + } + + /// Returns list of RecordPlaintext owned by view_key. + fn owned_records(&self, view_key: &ViewKey) -> Vec<RecordPlaintext> { + self.0 + .records() + .filter_map(|(_, rc)| rc.decrypt(view_key).ok().map(RecordPlaintext::from)) + .collect() + } + + /// Returns the RecordCiphertext for a commitment, or None. + fn find_record(&self, commitment: &Field) -> Option<RecordCiphertext> { + self.0.find_record(commitment).map(|rc| rc.clone().into()) + } + + /// Returns true if the transition contains the given commitment. + fn contains_commitment(&self, commitment: &Field) -> bool { + self.0.contains_commitment(commitment) + } + + /// Returns true if the transition contains the given serial number. + fn contains_serial_number(&self, serial_number: &Field) -> bool { + self.0.contains_serial_number(serial_number) + } + + /// Returns a list of dicts representing the transition inputs. + fn inputs(&self, py: Python) -> PyObject { + let list = PyList::empty(py); + for input in self.0.inputs().iter() { + let d = input_to_py_dict(py, input); + list.append(d).unwrap(); + } + list.into_py(py) + } + + /// Returns a list of dicts representing the transition outputs. + fn outputs(&self, py: Python) -> PyObject { + let list = PyList::empty(py); + for output in self.0.outputs().iter() { + let d = output_to_py_dict(py, output); + list.append(d).unwrap(); + } + list.into_py(py) + } + + /// Decrypt private inputs/outputs using the transition view key. + fn decrypt_transition(&self, tvk: &Field) -> anyhow::Result<Transition> { + let function_id = compute_function_id( + &U16Native::new(CurrentNetwork::ID), + self.0.program_id(), + self.0.function_name(), + )?; + + let mut decrypted_inputs = Vec::with_capacity(self.0.inputs().len()); + for (index, input) in self.0.inputs().iter().enumerate() { + decrypted_inputs.push(match input { + InputNative::Private(input_id, Some(ciphertext)) => { + let index_field = FieldNative::from_u16(index as u16); + let input_view_key = + CurrentNetwork::hash_psd4(&[function_id, **tvk, index_field]) + .map_err(|e| anyhow::anyhow!("{e}"))?; + let plaintext = ciphertext + .decrypt_symmetric(input_view_key) + .map_err(|e| anyhow::anyhow!("{e}"))?; + InputNative::Public(*input_id, Some(plaintext)) + } + _ => input.clone(), + }); + } + + let outputs = self.0.outputs(); + let num_inputs = self.0.inputs().len(); + let mut decrypted_outputs = Vec::with_capacity(outputs.len()); + for (index, output) in outputs.iter().enumerate() { + decrypted_outputs.push(match output { + OutputNative::Private(output_id, Some(ciphertext)) => { + let index_field = FieldNative::from_u16((num_inputs + index) as u16); + let output_view_key = + CurrentNetwork::hash_psd4(&[function_id, **tvk, index_field]) + .map_err(|e| anyhow::anyhow!("{e}"))?; + let plaintext = ciphertext + .decrypt_symmetric(output_view_key) + .map_err(|e| anyhow::anyhow!("{e}"))?; + OutputNative::Public(*output_id, Some(plaintext)) + } + _ => output.clone(), + }); + } + + TransitionNative::new( + *self.0.program_id(), + *self.0.function_name(), + decrypted_inputs, + decrypted_outputs, + *self.0.tpk(), + *self.0.tcm(), + *self.0.scm(), + ) + .map(Self) + .map_err(|e| anyhow::anyhow!("{e}")) + } +} + +// ---------- helpers ---------- + +fn input_to_py_dict<'py>(py: Python<'py>, input: &InputNative) -> &'py PyDict { + let d = PyDict::new(py); + match input { + InputNative::Constant(id, plaintext) => { + d.set_item("type", "constant").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + d.set_item("value", plaintext.as_ref().map(|p| p.to_string())) + .unwrap(); + } + InputNative::Public(id, plaintext) => { + d.set_item("type", "public").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + d.set_item("value", plaintext.as_ref().map(|p| p.to_string())) + .unwrap(); + } + InputNative::Private(id, ciphertext) => { + d.set_item("type", "private").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + d.set_item("value", ciphertext.as_ref().map(|c| c.to_string())) + .unwrap(); + } + InputNative::Record(serial_number, tag) => { + d.set_item("type", "record").unwrap(); + d.set_item("id", serial_number.to_string()).unwrap(); + d.set_item("tag", tag.to_string()).unwrap(); + } + InputNative::ExternalRecord(id) => { + d.set_item("type", "externalRecord").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + } + InputNative::DynamicRecord(id) => { + d.set_item("type", "record_dynamic").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + } + InputNative::RecordWithDynamicID(serial_number, tag, dynamic_id) => { + d.set_item("type", "record_with_dynamic_id").unwrap(); + d.set_item("id", serial_number.to_string()).unwrap(); + d.set_item("tag", tag.to_string()).unwrap(); + d.set_item("dynamic_id", dynamic_id.to_string()).unwrap(); + } + InputNative::ExternalRecordWithDynamicID(hash, dynamic_id) => { + d.set_item("type", "external_record_with_dynamic_id") + .unwrap(); + d.set_item("id", hash.to_string()).unwrap(); + d.set_item("dynamic_id", dynamic_id.to_string()).unwrap(); + } + } + d +} + +fn output_to_py_dict<'py>(py: Python<'py>, output: &OutputNative) -> &'py PyDict { + let d = PyDict::new(py); + match output { + OutputNative::Constant(id, plaintext) => { + d.set_item("type", "constant").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + d.set_item("value", plaintext.as_ref().map(|p| p.to_string())) + .unwrap(); + } + OutputNative::Public(id, plaintext) => { + d.set_item("type", "public").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + d.set_item("value", plaintext.as_ref().map(|p| p.to_string())) + .unwrap(); + } + OutputNative::Private(id, ciphertext) => { + d.set_item("type", "private").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + d.set_item("value", ciphertext.as_ref().map(|c| c.to_string())) + .unwrap(); + } + OutputNative::Record(commitment, checksum, record_ciphertext, sender_ciphertext) => { + d.set_item("type", "record").unwrap(); + d.set_item("id", commitment.to_string()).unwrap(); + d.set_item("checksum", checksum.to_string()).unwrap(); + d.set_item("value", record_ciphertext.as_ref().map(|r| r.to_string())) + .unwrap(); + d.set_item( + "sender_ciphertext", + sender_ciphertext.as_ref().map(|c| c.to_string()), + ) + .unwrap(); + } + OutputNative::ExternalRecord(id) => { + d.set_item("type", "external_record").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + } + OutputNative::Future(id, future) => { + if let Some(future) = future { + let obj = future_to_py_dict(py, future, id); + // copy all items from obj into d + for (k, v) in obj.iter() { + d.set_item(k, v).unwrap(); + } + } else { + d.set_item("type", "future").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + } + } + OutputNative::DynamicRecord(id) => { + d.set_item("type", "record_dynamic").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + } + OutputNative::RecordWithDynamicID( + commitment, + checksum, + record_ciphertext, + sender_ciphertext, + dynamic_id, + ) => { + d.set_item("type", "record_with_dynamic_id").unwrap(); + d.set_item("id", commitment.to_string()).unwrap(); + d.set_item("checksum", checksum.to_string()).unwrap(); + d.set_item("value", record_ciphertext.as_ref().map(|r| r.to_string())) + .unwrap(); + d.set_item( + "sender_ciphertext", + sender_ciphertext.as_ref().map(|c| c.to_string()), + ) + .unwrap(); + d.set_item("dynamic_id", dynamic_id.to_string()).unwrap(); + } + OutputNative::ExternalRecordWithDynamicID(hash, dynamic_id) => { + d.set_item("type", "external_record_with_dynamic_id") + .unwrap(); + d.set_item("id", hash.to_string()).unwrap(); + d.set_item("dynamic_id", dynamic_id.to_string()).unwrap(); + } + } + d +} + +fn future_to_py_dict<'py>(py: Python<'py>, future: &FutureNative, id: &FieldNative) -> &'py PyDict { + let arguments: Vec<PyObject> = future + .arguments() + .iter() + .map(|arg| argument_to_py_object(py, arg, id)) + .collect(); + let args_list = PyList::new(py, arguments); + let d = PyDict::new(py); + d.set_item("type", "future").unwrap(); + d.set_item("id", id.to_string()).unwrap(); + d.set_item("program", future.program_id().to_string()) + .unwrap(); + d.set_item("function", future.function_name().to_string()) + .unwrap(); + d.set_item("arguments", args_list).unwrap(); + d +} + +fn argument_to_py_object(py: Python<'_>, arg: &ArgumentNative, id: &FieldNative) -> PyObject { + match arg { + ArgumentNative::Plaintext(plaintext) => plaintext.to_string().into_py(py), + ArgumentNative::Future(future) => future_to_py_dict(py, future, id).into_py(py), + ArgumentNative::DynamicFuture(dynamic_future) => { + if let Ok(future) = dynamic_future.to_future() { + future_to_py_dict(py, &future, id).into_py(py) + } else { + let d = PyDict::new(py); + d.set_item("type", "dynamic_future").unwrap(); + d.set_item("checksum", dynamic_future.checksum().to_string()) + .unwrap(); + d.into_py(py) + } + } + } } impl From<TransitionNative> for Transition { @@ -104,3 +418,21 @@ impl From<TransitionNative> for Transition { Self(value) } } + +impl From<Transition> for TransitionNative { + fn from(value: Transition) -> Self { + value.0 + } +} + +impl From<&TransitionNative> for Transition { + fn from(value: &TransitionNative) -> Self { + Self(value.clone()) + } +} + +impl From<&Transition> for TransitionNative { + fn from(value: &Transition) -> Self { + value.0.clone() + } +} diff --git a/sdk/src/types.rs b/sdk/src/types.rs index 0ceebd9..f728b88 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -22,14 +22,15 @@ use snarkvm::circuit::network::AleoV0; use snarkvm::console::network::MainnetV0; #[cfg(feature = "testnet")] use snarkvm::console::network::TestnetV0; +use snarkvm::ledger::block::transition::{Input, Output}; use snarkvm::ledger::block::{Fee, Transaction}; use snarkvm::ledger::query::Query; use snarkvm::ledger::store::helpers::memory::BlockMemory; use snarkvm::prelude::{ - Address, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, GraphKey, Group, - Identifier, Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, ProvingKey, Record, - Response, Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, I128, I16, I32, I64, I8, - U128, U16, U32, U64, U8, + Address, Argument, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, Future, + GraphKey, Group, Identifier, Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, + ProvingKey, Record, Response, Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, + I128, I16, I32, I64, I8, U128, U16, U32, U64, U8, }; use snarkvm::synthesizer::{Process, Trace}; @@ -94,3 +95,9 @@ pub type CiphertextNative = Ciphertext<CurrentNetwork>; pub type PlaintextNative = Plaintext<CurrentNetwork>; pub type RecordCiphertextNative = Record<CurrentNetwork, CiphertextNative>; pub type RecordPlaintextNative = Record<CurrentNetwork, PlaintextNative>; + +// Transition I/O types +pub type InputNative = Input<CurrentNetwork>; +pub type OutputNative = Output<CurrentNetwork>; +pub type FutureNative = Future<CurrentNetwork>; +pub type ArgumentNative = Argument<CurrentNetwork>; From be2bec85d20d7fa8215d39568517d2c87cceeb63 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 11:57:55 -0400 Subject: [PATCH 40/72] feat: full wasm parity for integer, boolean, field, group, scalar types Implements W4d1: arithmetic, serialization, cross-cast, and conversion methods for all 15 algebraic types (Boolean, Field, Group, Scalar, U8-U128, I8-I128) matching TypeScript wasm API surface. Adds 142 parity tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 615 ++++++++++++++++++++++ sdk/python/aleo/_aleolib_mainnet.pyi | 615 ++++++++++++++++++++++ sdk/python/tests/test_numeric_parity.py | 636 +++++++++++++++++++++++ sdk/src/algebra/boolean.rs | 191 ++++++- sdk/src/algebra/field.rs | 114 ++++- sdk/src/algebra/group.rs | 119 ++++- sdk/src/algebra/integer.rs | 644 +++++++++++++++++++++++- sdk/src/algebra/scalar.rs | 161 +++++- 8 files changed, 3060 insertions(+), 35 deletions(-) create mode 100644 sdk/python/tests/test_numeric_parity.py diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index e4bf055..ccca0b2 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -114,6 +114,42 @@ class BHP1024: class Boolean: def __new__(cls, b: bool) -> Boolean: ... + @staticmethod + def from_string(s: str) -> Boolean: ... + @staticmethod + def random() -> Boolean: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Boolean: ... + def to_bytes_le(self) -> bytes: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> Boolean: ... + def to_bits_le(self) -> List[bool]: ... + def __bool__(self) -> bool: ... + def __invert__(self) -> Boolean: ... + def __and__(self, other: Boolean) -> Boolean: ... + def __or__(self, other: Boolean) -> Boolean: ... + def __xor__(self, other: Boolean) -> Boolean: ... + def not_(self) -> Boolean: ... + def and_(self, other: Boolean) -> Boolean: ... + def or_(self, other: Boolean) -> Boolean: ... + def xor(self, other: Boolean) -> Boolean: ... + def nand(self, other: Boolean) -> Boolean: ... + def nor(self, other: Boolean) -> Boolean: ... + def to_plaintext(self) -> Plaintext: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_group(self) -> Group: ... + def to_address(self) -> Address: ... + def to_u8(self) -> U8: ... + def to_u16(self) -> U16: ... + def to_u32(self) -> U32: ... + def to_u64(self) -> U64: ... + def to_u128(self) -> U128: ... + def to_i8(self) -> I8: ... + def to_i16(self) -> I16: ... + def to_i32(self) -> I32: ... + def to_i64(self) -> I64: ... + def to_i128(self) -> I128: ... class Ciphertext: @@ -200,6 +236,22 @@ class Field: def negate(self) -> Field: ... def to_scalar_lossy(self) -> Scalar: ... def to_group(self) -> Group: ... + def to_group_lossy(self) -> Group: ... + def to_address(self) -> Address: ... + def to_address_lossy(self) -> Address: ... + def to_boolean(self) -> Boolean: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_plaintext(self) -> Plaintext: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... def __add__(self, other: Field) -> Field: ... def __sub__(self, other: Field) -> Field: ... def __mul__(self, other: Field) -> Field: ... @@ -226,13 +278,33 @@ class Group: @staticmethod def generator() -> Group: ... @staticmethod + def random() -> Group: ... + @staticmethod def from_field(field: Field) -> Group: ... @staticmethod def from_bytes_le(bytes: bytes) -> Group: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> Group: ... def to_x_coordinate(self) -> Field: ... + def to_field(self) -> Field: ... def to_address(self) -> Address: ... def double(self) -> Group: ... def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_fields(self) -> List[Field]: ... + def to_plaintext(self) -> Plaintext: ... + def to_scalar_lossy(self) -> Scalar: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... def add(self, other: Group) -> Group: ... def subtract(self, other: Group) -> Group: ... def negate(self) -> Group: ... @@ -251,36 +323,306 @@ class Identifier: class I8: def __new__(cls, value: int) -> I8: ... @staticmethod + def from_string(s: str) -> I8: ... + @staticmethod def zero() -> I8: ... + @staticmethod + def one() -> I8: ... + @staticmethod + def random() -> I8: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I8: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> I8: ... + @staticmethod + def from_field(f: Field) -> I8: ... + @staticmethod + def from_field_lossy(f: Field) -> I8: ... + @staticmethod + def from_fields(fields: List[Field]) -> I8: ... def __int__(self) -> int: ... + def __add__(self, other: I8) -> I8: ... + def __sub__(self, other: I8) -> I8: ... + def __mul__(self, other: I8) -> I8: ... + def __floordiv__(self, other: I8) -> I8: ... + def __pow__(self, exp: U32, modulo: None = None) -> I8: ... + def __neg__(self) -> I8: ... + def add(self, other: I8) -> I8: ... + def subtract(self, other: I8) -> I8: ... + def multiply(self, other: I8) -> I8: ... + def divide(self, other: I8) -> I8: ... + def negate(self) -> I8: ... + def abs_checked(self) -> I8: ... + def abs_wrapped(self) -> I8: ... + def add_wrapped(self, other: I8) -> I8: ... + def sub_wrapped(self, other: I8) -> I8: ... + def mul_wrapped(self, other: I8) -> I8: ... + def div_wrapped(self, other: I8) -> I8: ... + def rem(self, other: I8) -> I8: ... + def rem_wrapped(self, other: I8) -> I8: ... + def pow_u8(self, exp: U8) -> I8: ... + def pow_u16(self, exp: U16) -> I8: ... + def pow_u32(self, exp: U32) -> I8: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I16: def __new__(cls, value: int) -> I16: ... @staticmethod + def from_string(s: str) -> I16: ... + @staticmethod def zero() -> I16: ... + @staticmethod + def one() -> I16: ... + @staticmethod + def random() -> I16: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I16: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> I16: ... + @staticmethod + def from_field(f: Field) -> I16: ... + @staticmethod + def from_field_lossy(f: Field) -> I16: ... + @staticmethod + def from_fields(fields: List[Field]) -> I16: ... def __int__(self) -> int: ... + def __add__(self, other: I16) -> I16: ... + def __sub__(self, other: I16) -> I16: ... + def __mul__(self, other: I16) -> I16: ... + def __floordiv__(self, other: I16) -> I16: ... + def __pow__(self, exp: U32, modulo: None = None) -> I16: ... + def __neg__(self) -> I16: ... + def add(self, other: I16) -> I16: ... + def subtract(self, other: I16) -> I16: ... + def multiply(self, other: I16) -> I16: ... + def divide(self, other: I16) -> I16: ... + def negate(self) -> I16: ... + def abs_checked(self) -> I16: ... + def abs_wrapped(self) -> I16: ... + def add_wrapped(self, other: I16) -> I16: ... + def sub_wrapped(self, other: I16) -> I16: ... + def mul_wrapped(self, other: I16) -> I16: ... + def div_wrapped(self, other: I16) -> I16: ... + def rem(self, other: I16) -> I16: ... + def rem_wrapped(self, other: I16) -> I16: ... + def pow_u8(self, exp: U8) -> I16: ... + def pow_u16(self, exp: U16) -> I16: ... + def pow_u32(self, exp: U32) -> I16: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I32: def __new__(cls, value: int) -> I32: ... @staticmethod + def from_string(s: str) -> I32: ... + @staticmethod def zero() -> I32: ... + @staticmethod + def one() -> I32: ... + @staticmethod + def random() -> I32: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I32: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> I32: ... + @staticmethod + def from_field(f: Field) -> I32: ... + @staticmethod + def from_field_lossy(f: Field) -> I32: ... + @staticmethod + def from_fields(fields: List[Field]) -> I32: ... def __int__(self) -> int: ... + def __add__(self, other: I32) -> I32: ... + def __sub__(self, other: I32) -> I32: ... + def __mul__(self, other: I32) -> I32: ... + def __floordiv__(self, other: I32) -> I32: ... + def __pow__(self, exp: U32, modulo: None = None) -> I32: ... + def __neg__(self) -> I32: ... + def add(self, other: I32) -> I32: ... + def subtract(self, other: I32) -> I32: ... + def multiply(self, other: I32) -> I32: ... + def divide(self, other: I32) -> I32: ... + def negate(self) -> I32: ... + def abs_checked(self) -> I32: ... + def abs_wrapped(self) -> I32: ... + def add_wrapped(self, other: I32) -> I32: ... + def sub_wrapped(self, other: I32) -> I32: ... + def mul_wrapped(self, other: I32) -> I32: ... + def div_wrapped(self, other: I32) -> I32: ... + def rem(self, other: I32) -> I32: ... + def rem_wrapped(self, other: I32) -> I32: ... + def pow_u8(self, exp: U8) -> I32: ... + def pow_u16(self, exp: U16) -> I32: ... + def pow_u32(self, exp: U32) -> I32: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I64: def __new__(cls, value: int) -> I64: ... @staticmethod + def from_string(s: str) -> I64: ... + @staticmethod def zero() -> I64: ... + @staticmethod + def one() -> I64: ... + @staticmethod + def random() -> I64: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I64: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> I64: ... + @staticmethod + def from_field(f: Field) -> I64: ... + @staticmethod + def from_field_lossy(f: Field) -> I64: ... + @staticmethod + def from_fields(fields: List[Field]) -> I64: ... def __int__(self) -> int: ... + def __add__(self, other: I64) -> I64: ... + def __sub__(self, other: I64) -> I64: ... + def __mul__(self, other: I64) -> I64: ... + def __floordiv__(self, other: I64) -> I64: ... + def __pow__(self, exp: U32, modulo: None = None) -> I64: ... + def __neg__(self) -> I64: ... + def add(self, other: I64) -> I64: ... + def subtract(self, other: I64) -> I64: ... + def multiply(self, other: I64) -> I64: ... + def divide(self, other: I64) -> I64: ... + def negate(self) -> I64: ... + def abs_checked(self) -> I64: ... + def abs_wrapped(self) -> I64: ... + def add_wrapped(self, other: I64) -> I64: ... + def sub_wrapped(self, other: I64) -> I64: ... + def mul_wrapped(self, other: I64) -> I64: ... + def div_wrapped(self, other: I64) -> I64: ... + def rem(self, other: I64) -> I64: ... + def rem_wrapped(self, other: I64) -> I64: ... + def pow_u8(self, exp: U8) -> I64: ... + def pow_u16(self, exp: U16) -> I64: ... + def pow_u32(self, exp: U32) -> I64: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I128: def __new__(cls, value: int) -> I128: ... @staticmethod + def from_string(s: str) -> I128: ... + @staticmethod def zero() -> I128: ... + @staticmethod + def one() -> I128: ... + @staticmethod + def random() -> I128: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I128: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> I128: ... + @staticmethod + def from_field(f: Field) -> I128: ... + @staticmethod + def from_field_lossy(f: Field) -> I128: ... + @staticmethod + def from_fields(fields: List[Field]) -> I128: ... def __int__(self) -> int: ... + def __add__(self, other: I128) -> I128: ... + def __sub__(self, other: I128) -> I128: ... + def __mul__(self, other: I128) -> I128: ... + def __floordiv__(self, other: I128) -> I128: ... + def __pow__(self, exp: U32, modulo: None = None) -> I128: ... + def __neg__(self) -> I128: ... + def add(self, other: I128) -> I128: ... + def subtract(self, other: I128) -> I128: ... + def multiply(self, other: I128) -> I128: ... + def divide(self, other: I128) -> I128: ... + def negate(self) -> I128: ... + def abs_checked(self) -> I128: ... + def abs_wrapped(self) -> I128: ... + def add_wrapped(self, other: I128) -> I128: ... + def sub_wrapped(self, other: I128) -> I128: ... + def mul_wrapped(self, other: I128) -> I128: ... + def div_wrapped(self, other: I128) -> I128: ... + def rem(self, other: I128) -> I128: ... + def rem_wrapped(self, other: I128) -> I128: ... + def pow_u8(self, exp: U8) -> I128: ... + def pow_u16(self, exp: U16) -> I128: ... + def pow_u32(self, exp: U32) -> I128: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class Literal: @@ -578,15 +920,38 @@ class Scalar: @staticmethod def one() -> Scalar: ... @staticmethod + def random() -> Scalar: ... + @staticmethod def from_bytes_le(bytes: bytes) -> Scalar: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> Scalar: ... def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... def to_field(self) -> Field: ... + def to_plaintext(self) -> Plaintext: ... + def double(self) -> Scalar: ... def add(self, other: Scalar) -> Scalar: ... def subtract(self, other: Scalar) -> Scalar: ... def multiply(self, other: Scalar) -> Scalar: ... def divide(self, other: Scalar) -> Scalar: ... def pow(self, other: Scalar) -> Scalar: ... def negate(self) -> Scalar: ... + def to_boolean(self) -> Boolean: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_group(self) -> Group: ... + def to_group_lossy(self) -> Group: ... + def to_address(self) -> Address: ... + def to_address_lossy(self) -> Address: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... def __pow__(self, other: Scalar) -> Scalar: ... def __neg__(self) -> Scalar: ... def __add__(self, other: Scalar) -> Scalar: ... @@ -703,36 +1068,286 @@ class Transition: class U8: def __new__(cls, value: int) -> U8: ... @staticmethod + def from_string(s: str) -> U8: ... + @staticmethod def zero() -> U8: ... + @staticmethod + def one() -> U8: ... + @staticmethod + def random() -> U8: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U8: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> U8: ... + @staticmethod + def from_field(f: Field) -> U8: ... + @staticmethod + def from_field_lossy(f: Field) -> U8: ... + @staticmethod + def from_fields(fields: List[Field]) -> U8: ... def __int__(self) -> int: ... + def __add__(self, other: U8) -> U8: ... + def __sub__(self, other: U8) -> U8: ... + def __mul__(self, other: U8) -> U8: ... + def __floordiv__(self, other: U8) -> U8: ... + def __pow__(self, exp: U32, modulo: None = None) -> U8: ... + def add(self, other: U8) -> U8: ... + def subtract(self, other: U8) -> U8: ... + def multiply(self, other: U8) -> U8: ... + def divide(self, other: U8) -> U8: ... + def add_wrapped(self, other: U8) -> U8: ... + def sub_wrapped(self, other: U8) -> U8: ... + def mul_wrapped(self, other: U8) -> U8: ... + def div_wrapped(self, other: U8) -> U8: ... + def rem(self, other: U8) -> U8: ... + def rem_wrapped(self, other: U8) -> U8: ... + def pow_u8(self, exp: U8) -> U8: ... + def pow_u16(self, exp: U16) -> U8: ... + def pow_u32(self, exp: U32) -> U8: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U16: def __new__(cls, value: int) -> U16: ... @staticmethod + def from_string(s: str) -> U16: ... + @staticmethod def zero() -> U16: ... + @staticmethod + def one() -> U16: ... + @staticmethod + def random() -> U16: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U16: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> U16: ... + @staticmethod + def from_field(f: Field) -> U16: ... + @staticmethod + def from_field_lossy(f: Field) -> U16: ... + @staticmethod + def from_fields(fields: List[Field]) -> U16: ... def __int__(self) -> int: ... + def __add__(self, other: U16) -> U16: ... + def __sub__(self, other: U16) -> U16: ... + def __mul__(self, other: U16) -> U16: ... + def __floordiv__(self, other: U16) -> U16: ... + def __pow__(self, exp: U32, modulo: None = None) -> U16: ... + def add(self, other: U16) -> U16: ... + def subtract(self, other: U16) -> U16: ... + def multiply(self, other: U16) -> U16: ... + def divide(self, other: U16) -> U16: ... + def add_wrapped(self, other: U16) -> U16: ... + def sub_wrapped(self, other: U16) -> U16: ... + def mul_wrapped(self, other: U16) -> U16: ... + def div_wrapped(self, other: U16) -> U16: ... + def rem(self, other: U16) -> U16: ... + def rem_wrapped(self, other: U16) -> U16: ... + def pow_u8(self, exp: U8) -> U16: ... + def pow_u16(self, exp: U16) -> U16: ... + def pow_u32(self, exp: U32) -> U16: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U32: def __new__(cls, value: int) -> U32: ... @staticmethod + def from_string(s: str) -> U32: ... + @staticmethod def zero() -> U32: ... + @staticmethod + def one() -> U32: ... + @staticmethod + def random() -> U32: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U32: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> U32: ... + @staticmethod + def from_field(f: Field) -> U32: ... + @staticmethod + def from_field_lossy(f: Field) -> U32: ... + @staticmethod + def from_fields(fields: List[Field]) -> U32: ... def __int__(self) -> int: ... + def __add__(self, other: U32) -> U32: ... + def __sub__(self, other: U32) -> U32: ... + def __mul__(self, other: U32) -> U32: ... + def __floordiv__(self, other: U32) -> U32: ... + def __pow__(self, exp: U32, modulo: None = None) -> U32: ... + def add(self, other: U32) -> U32: ... + def subtract(self, other: U32) -> U32: ... + def multiply(self, other: U32) -> U32: ... + def divide(self, other: U32) -> U32: ... + def add_wrapped(self, other: U32) -> U32: ... + def sub_wrapped(self, other: U32) -> U32: ... + def mul_wrapped(self, other: U32) -> U32: ... + def div_wrapped(self, other: U32) -> U32: ... + def rem(self, other: U32) -> U32: ... + def rem_wrapped(self, other: U32) -> U32: ... + def pow_u8(self, exp: U8) -> U32: ... + def pow_u16(self, exp: U16) -> U32: ... + def pow_u32(self, exp: U32) -> U32: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U64: def __new__(cls, value: int) -> U64: ... @staticmethod + def from_string(s: str) -> U64: ... + @staticmethod def zero() -> U64: ... + @staticmethod + def one() -> U64: ... + @staticmethod + def random() -> U64: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U64: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> U64: ... + @staticmethod + def from_field(f: Field) -> U64: ... + @staticmethod + def from_field_lossy(f: Field) -> U64: ... + @staticmethod + def from_fields(fields: List[Field]) -> U64: ... def __int__(self) -> int: ... + def __add__(self, other: U64) -> U64: ... + def __sub__(self, other: U64) -> U64: ... + def __mul__(self, other: U64) -> U64: ... + def __floordiv__(self, other: U64) -> U64: ... + def __pow__(self, exp: U32, modulo: None = None) -> U64: ... + def add(self, other: U64) -> U64: ... + def subtract(self, other: U64) -> U64: ... + def multiply(self, other: U64) -> U64: ... + def divide(self, other: U64) -> U64: ... + def add_wrapped(self, other: U64) -> U64: ... + def sub_wrapped(self, other: U64) -> U64: ... + def mul_wrapped(self, other: U64) -> U64: ... + def div_wrapped(self, other: U64) -> U64: ... + def rem(self, other: U64) -> U64: ... + def rem_wrapped(self, other: U64) -> U64: ... + def pow_u8(self, exp: U8) -> U64: ... + def pow_u16(self, exp: U16) -> U64: ... + def pow_u32(self, exp: U32) -> U64: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U128: def __new__(cls, value: int) -> U128: ... @staticmethod + def from_string(s: str) -> U128: ... + @staticmethod def zero() -> U128: ... + @staticmethod + def one() -> U128: ... + @staticmethod + def random() -> U128: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U128: ... + @staticmethod + def from_bits_le(bits: List[bool]) -> U128: ... + @staticmethod + def from_field(f: Field) -> U128: ... + @staticmethod + def from_field_lossy(f: Field) -> U128: ... + @staticmethod + def from_fields(fields: List[Field]) -> U128: ... def __int__(self) -> int: ... + def __add__(self, other: U128) -> U128: ... + def __sub__(self, other: U128) -> U128: ... + def __mul__(self, other: U128) -> U128: ... + def __floordiv__(self, other: U128) -> U128: ... + def __pow__(self, exp: U32, modulo: None = None) -> U128: ... + def add(self, other: U128) -> U128: ... + def subtract(self, other: U128) -> U128: ... + def multiply(self, other: U128) -> U128: ... + def divide(self, other: U128) -> U128: ... + def add_wrapped(self, other: U128) -> U128: ... + def sub_wrapped(self, other: U128) -> U128: ... + def mul_wrapped(self, other: U128) -> U128: ... + def div_wrapped(self, other: U128) -> U128: ... + def rem(self, other: U128) -> U128: ... + def rem_wrapped(self, other: U128) -> U128: ... + def pow_u8(self, exp: U8) -> U128: ... + def pow_u16(self, exp: U16) -> U128: ... + def pow_u32(self, exp: U32) -> U128: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> List[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class Value: diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index bf96811..d39f80a 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -114,6 +114,42 @@ class BHP1024: class Boolean: def __new__(cls, b: bool) -> Boolean: ... + @staticmethod + def from_string(s: str) -> Boolean: ... + @staticmethod + def random() -> Boolean: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> Boolean: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> Boolean: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def not_(self) -> Boolean: ... + def and_(self, other: Boolean) -> Boolean: ... + def or_(self, other: Boolean) -> Boolean: ... + def xor(self, other: Boolean) -> Boolean: ... + def nand(self, other: Boolean) -> Boolean: ... + def nor(self, other: Boolean) -> Boolean: ... + def __invert__(self) -> Boolean: ... + def __and__(self, other: Boolean) -> Boolean: ... + def __or__(self, other: Boolean) -> Boolean: ... + def __xor__(self, other: Boolean) -> Boolean: ... + def __bool__(self) -> bool: ... + def to_plaintext(self) -> Plaintext: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_group(self) -> Group: ... + def to_address(self) -> Address: ... + def to_u8(self) -> U8: ... + def to_u16(self) -> U16: ... + def to_u32(self) -> U32: ... + def to_u64(self) -> U64: ... + def to_u128(self) -> U128: ... + def to_i8(self) -> I8: ... + def to_i16(self) -> I16: ... + def to_i32(self) -> I32: ... + def to_i64(self) -> I64: ... + def to_i128(self) -> I128: ... class Ciphertext: @@ -200,6 +236,22 @@ class Field: def __pow__(self, other: Field) -> Field: ... def __neg__(self) -> Field: ... def to_bits_le(self) -> list[bool]: ... + def to_group_lossy(self) -> Group: ... + def to_address(self) -> Address: ... + def to_address_lossy(self) -> Address: ... + def to_boolean(self) -> Boolean: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_plaintext(self) -> Plaintext: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class GraphKey: @@ -234,6 +286,26 @@ class Group: def __sub__(self, other: Group) -> Group: ... def __neg__(self) -> Group: ... def __mul__(self, scalar: Scalar) -> Group: ... + @staticmethod + def random() -> Group: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> Group: ... + def to_bits_le(self) -> list[bool]: ... + def to_fields(self) -> list[Field]: ... + def to_plaintext(self) -> Plaintext: ... + def to_field(self) -> Field: ... + def to_scalar_lossy(self) -> Scalar: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class Identifier: @@ -244,36 +316,306 @@ class Identifier: class I8: def __new__(cls, value: int) -> I8: ... @staticmethod + def from_string(s: str) -> I8: ... + @staticmethod def zero() -> I8: ... + @staticmethod + def one() -> I8: ... + @staticmethod + def random() -> I8: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I8: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> I8: ... + @staticmethod + def from_field(f: Field) -> I8: ... + @staticmethod + def from_field_lossy(f: Field) -> I8: ... + @staticmethod + def from_fields(fields: list[Field]) -> I8: ... def __int__(self) -> int: ... + def __add__(self, other: I8) -> I8: ... + def __sub__(self, other: I8) -> I8: ... + def __mul__(self, other: I8) -> I8: ... + def __floordiv__(self, other: I8) -> I8: ... + def __pow__(self, exp: U32, modulo: None = None) -> I8: ... + def __neg__(self) -> I8: ... + def add(self, other: I8) -> I8: ... + def subtract(self, other: I8) -> I8: ... + def multiply(self, other: I8) -> I8: ... + def divide(self, other: I8) -> I8: ... + def negate(self) -> I8: ... + def abs_checked(self) -> I8: ... + def abs_wrapped(self) -> I8: ... + def add_wrapped(self, other: I8) -> I8: ... + def sub_wrapped(self, other: I8) -> I8: ... + def mul_wrapped(self, other: I8) -> I8: ... + def div_wrapped(self, other: I8) -> I8: ... + def rem(self, other: I8) -> I8: ... + def rem_wrapped(self, other: I8) -> I8: ... + def pow_u8(self, exp: U8) -> I8: ... + def pow_u16(self, exp: U16) -> I8: ... + def pow_u32(self, exp: U32) -> I8: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I16: def __new__(cls, value: int) -> I16: ... @staticmethod + def from_string(s: str) -> I16: ... + @staticmethod def zero() -> I16: ... + @staticmethod + def one() -> I16: ... + @staticmethod + def random() -> I16: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I16: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> I16: ... + @staticmethod + def from_field(f: Field) -> I16: ... + @staticmethod + def from_field_lossy(f: Field) -> I16: ... + @staticmethod + def from_fields(fields: list[Field]) -> I16: ... def __int__(self) -> int: ... + def __add__(self, other: I16) -> I16: ... + def __sub__(self, other: I16) -> I16: ... + def __mul__(self, other: I16) -> I16: ... + def __floordiv__(self, other: I16) -> I16: ... + def __pow__(self, exp: U32, modulo: None = None) -> I16: ... + def __neg__(self) -> I16: ... + def add(self, other: I16) -> I16: ... + def subtract(self, other: I16) -> I16: ... + def multiply(self, other: I16) -> I16: ... + def divide(self, other: I16) -> I16: ... + def negate(self) -> I16: ... + def abs_checked(self) -> I16: ... + def abs_wrapped(self) -> I16: ... + def add_wrapped(self, other: I16) -> I16: ... + def sub_wrapped(self, other: I16) -> I16: ... + def mul_wrapped(self, other: I16) -> I16: ... + def div_wrapped(self, other: I16) -> I16: ... + def rem(self, other: I16) -> I16: ... + def rem_wrapped(self, other: I16) -> I16: ... + def pow_u8(self, exp: U8) -> I16: ... + def pow_u16(self, exp: U16) -> I16: ... + def pow_u32(self, exp: U32) -> I16: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I32: def __new__(cls, value: int) -> I32: ... @staticmethod + def from_string(s: str) -> I32: ... + @staticmethod def zero() -> I32: ... + @staticmethod + def one() -> I32: ... + @staticmethod + def random() -> I32: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I32: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> I32: ... + @staticmethod + def from_field(f: Field) -> I32: ... + @staticmethod + def from_field_lossy(f: Field) -> I32: ... + @staticmethod + def from_fields(fields: list[Field]) -> I32: ... def __int__(self) -> int: ... + def __add__(self, other: I32) -> I32: ... + def __sub__(self, other: I32) -> I32: ... + def __mul__(self, other: I32) -> I32: ... + def __floordiv__(self, other: I32) -> I32: ... + def __pow__(self, exp: U32, modulo: None = None) -> I32: ... + def __neg__(self) -> I32: ... + def add(self, other: I32) -> I32: ... + def subtract(self, other: I32) -> I32: ... + def multiply(self, other: I32) -> I32: ... + def divide(self, other: I32) -> I32: ... + def negate(self) -> I32: ... + def abs_checked(self) -> I32: ... + def abs_wrapped(self) -> I32: ... + def add_wrapped(self, other: I32) -> I32: ... + def sub_wrapped(self, other: I32) -> I32: ... + def mul_wrapped(self, other: I32) -> I32: ... + def div_wrapped(self, other: I32) -> I32: ... + def rem(self, other: I32) -> I32: ... + def rem_wrapped(self, other: I32) -> I32: ... + def pow_u8(self, exp: U8) -> I32: ... + def pow_u16(self, exp: U16) -> I32: ... + def pow_u32(self, exp: U32) -> I32: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I64: def __new__(cls, value: int) -> I64: ... @staticmethod + def from_string(s: str) -> I64: ... + @staticmethod def zero() -> I64: ... + @staticmethod + def one() -> I64: ... + @staticmethod + def random() -> I64: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I64: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> I64: ... + @staticmethod + def from_field(f: Field) -> I64: ... + @staticmethod + def from_field_lossy(f: Field) -> I64: ... + @staticmethod + def from_fields(fields: list[Field]) -> I64: ... def __int__(self) -> int: ... + def __add__(self, other: I64) -> I64: ... + def __sub__(self, other: I64) -> I64: ... + def __mul__(self, other: I64) -> I64: ... + def __floordiv__(self, other: I64) -> I64: ... + def __pow__(self, exp: U32, modulo: None = None) -> I64: ... + def __neg__(self) -> I64: ... + def add(self, other: I64) -> I64: ... + def subtract(self, other: I64) -> I64: ... + def multiply(self, other: I64) -> I64: ... + def divide(self, other: I64) -> I64: ... + def negate(self) -> I64: ... + def abs_checked(self) -> I64: ... + def abs_wrapped(self) -> I64: ... + def add_wrapped(self, other: I64) -> I64: ... + def sub_wrapped(self, other: I64) -> I64: ... + def mul_wrapped(self, other: I64) -> I64: ... + def div_wrapped(self, other: I64) -> I64: ... + def rem(self, other: I64) -> I64: ... + def rem_wrapped(self, other: I64) -> I64: ... + def pow_u8(self, exp: U8) -> I64: ... + def pow_u16(self, exp: U16) -> I64: ... + def pow_u32(self, exp: U32) -> I64: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class I128: def __new__(cls, value: int) -> I128: ... @staticmethod + def from_string(s: str) -> I128: ... + @staticmethod def zero() -> I128: ... + @staticmethod + def one() -> I128: ... + @staticmethod + def random() -> I128: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> I128: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> I128: ... + @staticmethod + def from_field(f: Field) -> I128: ... + @staticmethod + def from_field_lossy(f: Field) -> I128: ... + @staticmethod + def from_fields(fields: list[Field]) -> I128: ... def __int__(self) -> int: ... + def __add__(self, other: I128) -> I128: ... + def __sub__(self, other: I128) -> I128: ... + def __mul__(self, other: I128) -> I128: ... + def __floordiv__(self, other: I128) -> I128: ... + def __pow__(self, exp: U32, modulo: None = None) -> I128: ... + def __neg__(self) -> I128: ... + def add(self, other: I128) -> I128: ... + def subtract(self, other: I128) -> I128: ... + def multiply(self, other: I128) -> I128: ... + def divide(self, other: I128) -> I128: ... + def negate(self) -> I128: ... + def abs_checked(self) -> I128: ... + def abs_wrapped(self) -> I128: ... + def add_wrapped(self, other: I128) -> I128: ... + def sub_wrapped(self, other: I128) -> I128: ... + def mul_wrapped(self, other: I128) -> I128: ... + def div_wrapped(self, other: I128) -> I128: ... + def rem(self, other: I128) -> I128: ... + def rem_wrapped(self, other: I128) -> I128: ... + def pow_u8(self, exp: U8) -> I128: ... + def pow_u16(self, exp: U16) -> I128: ... + def pow_u32(self, exp: U32) -> I128: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class Literal: @@ -567,15 +909,38 @@ class Scalar: @staticmethod def one() -> Scalar: ... @staticmethod + def random() -> Scalar: ... + @staticmethod def from_bytes_le(bytes: bytes) -> Scalar: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> Scalar: ... def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... def to_field(self) -> Field: ... + def to_plaintext(self) -> Plaintext: ... + def double(self) -> Scalar: ... def add(self, other: Scalar) -> Scalar: ... def subtract(self, other: Scalar) -> Scalar: ... def multiply(self, other: Scalar) -> Scalar: ... def divide(self, other: Scalar) -> Scalar: ... def pow(self, other: Scalar) -> Scalar: ... def negate(self) -> Scalar: ... + def to_boolean(self) -> Boolean: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_group(self) -> Group: ... + def to_group_lossy(self) -> Group: ... + def to_address(self) -> Address: ... + def to_address_lossy(self) -> Address: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... def __pow__(self, other: Scalar) -> Scalar: ... def __neg__(self) -> Scalar: ... def __add__(self, other: Scalar) -> Scalar: ... @@ -692,36 +1057,286 @@ class Transition: class U8: def __new__(cls, value: int) -> U8: ... @staticmethod + def from_string(s: str) -> U8: ... + @staticmethod def zero() -> U8: ... + @staticmethod + def one() -> U8: ... + @staticmethod + def random() -> U8: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U8: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> U8: ... + @staticmethod + def from_field(f: Field) -> U8: ... + @staticmethod + def from_field_lossy(f: Field) -> U8: ... + @staticmethod + def from_fields(fields: list[Field]) -> U8: ... def __int__(self) -> int: ... + def __add__(self, other: U8) -> U8: ... + def __sub__(self, other: U8) -> U8: ... + def __mul__(self, other: U8) -> U8: ... + def __floordiv__(self, other: U8) -> U8: ... + def __pow__(self, exp: U32, modulo: None = None) -> U8: ... + def add(self, other: U8) -> U8: ... + def subtract(self, other: U8) -> U8: ... + def multiply(self, other: U8) -> U8: ... + def divide(self, other: U8) -> U8: ... + def add_wrapped(self, other: U8) -> U8: ... + def sub_wrapped(self, other: U8) -> U8: ... + def mul_wrapped(self, other: U8) -> U8: ... + def div_wrapped(self, other: U8) -> U8: ... + def rem(self, other: U8) -> U8: ... + def rem_wrapped(self, other: U8) -> U8: ... + def pow_u8(self, exp: U8) -> U8: ... + def pow_u16(self, exp: U16) -> U8: ... + def pow_u32(self, exp: U32) -> U8: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U16: def __new__(cls, value: int) -> U16: ... @staticmethod + def from_string(s: str) -> U16: ... + @staticmethod def zero() -> U16: ... + @staticmethod + def one() -> U16: ... + @staticmethod + def random() -> U16: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U16: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> U16: ... + @staticmethod + def from_field(f: Field) -> U16: ... + @staticmethod + def from_field_lossy(f: Field) -> U16: ... + @staticmethod + def from_fields(fields: list[Field]) -> U16: ... def __int__(self) -> int: ... + def __add__(self, other: U16) -> U16: ... + def __sub__(self, other: U16) -> U16: ... + def __mul__(self, other: U16) -> U16: ... + def __floordiv__(self, other: U16) -> U16: ... + def __pow__(self, exp: U32, modulo: None = None) -> U16: ... + def add(self, other: U16) -> U16: ... + def subtract(self, other: U16) -> U16: ... + def multiply(self, other: U16) -> U16: ... + def divide(self, other: U16) -> U16: ... + def add_wrapped(self, other: U16) -> U16: ... + def sub_wrapped(self, other: U16) -> U16: ... + def mul_wrapped(self, other: U16) -> U16: ... + def div_wrapped(self, other: U16) -> U16: ... + def rem(self, other: U16) -> U16: ... + def rem_wrapped(self, other: U16) -> U16: ... + def pow_u8(self, exp: U8) -> U16: ... + def pow_u16(self, exp: U16) -> U16: ... + def pow_u32(self, exp: U32) -> U16: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U32: def __new__(cls, value: int) -> U32: ... @staticmethod + def from_string(s: str) -> U32: ... + @staticmethod def zero() -> U32: ... + @staticmethod + def one() -> U32: ... + @staticmethod + def random() -> U32: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U32: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> U32: ... + @staticmethod + def from_field(f: Field) -> U32: ... + @staticmethod + def from_field_lossy(f: Field) -> U32: ... + @staticmethod + def from_fields(fields: list[Field]) -> U32: ... def __int__(self) -> int: ... + def __add__(self, other: U32) -> U32: ... + def __sub__(self, other: U32) -> U32: ... + def __mul__(self, other: U32) -> U32: ... + def __floordiv__(self, other: U32) -> U32: ... + def __pow__(self, exp: U32, modulo: None = None) -> U32: ... + def add(self, other: U32) -> U32: ... + def subtract(self, other: U32) -> U32: ... + def multiply(self, other: U32) -> U32: ... + def divide(self, other: U32) -> U32: ... + def add_wrapped(self, other: U32) -> U32: ... + def sub_wrapped(self, other: U32) -> U32: ... + def mul_wrapped(self, other: U32) -> U32: ... + def div_wrapped(self, other: U32) -> U32: ... + def rem(self, other: U32) -> U32: ... + def rem_wrapped(self, other: U32) -> U32: ... + def pow_u8(self, exp: U8) -> U32: ... + def pow_u16(self, exp: U16) -> U32: ... + def pow_u32(self, exp: U32) -> U32: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U64: def __new__(cls, value: int) -> U64: ... @staticmethod + def from_string(s: str) -> U64: ... + @staticmethod def zero() -> U64: ... + @staticmethod + def one() -> U64: ... + @staticmethod + def random() -> U64: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U64: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> U64: ... + @staticmethod + def from_field(f: Field) -> U64: ... + @staticmethod + def from_field_lossy(f: Field) -> U64: ... + @staticmethod + def from_fields(fields: list[Field]) -> U64: ... def __int__(self) -> int: ... + def __add__(self, other: U64) -> U64: ... + def __sub__(self, other: U64) -> U64: ... + def __mul__(self, other: U64) -> U64: ... + def __floordiv__(self, other: U64) -> U64: ... + def __pow__(self, exp: U32, modulo: None = None) -> U64: ... + def add(self, other: U64) -> U64: ... + def subtract(self, other: U64) -> U64: ... + def multiply(self, other: U64) -> U64: ... + def divide(self, other: U64) -> U64: ... + def add_wrapped(self, other: U64) -> U64: ... + def sub_wrapped(self, other: U64) -> U64: ... + def mul_wrapped(self, other: U64) -> U64: ... + def div_wrapped(self, other: U64) -> U64: ... + def rem(self, other: U64) -> U64: ... + def rem_wrapped(self, other: U64) -> U64: ... + def pow_u8(self, exp: U8) -> U64: ... + def pow_u16(self, exp: U16) -> U64: ... + def pow_u32(self, exp: U32) -> U64: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class U128: def __new__(cls, value: int) -> U128: ... @staticmethod + def from_string(s: str) -> U128: ... + @staticmethod def zero() -> U128: ... + @staticmethod + def one() -> U128: ... + @staticmethod + def random() -> U128: ... + @staticmethod + def from_bytes_le(bytes: bytes) -> U128: ... + @staticmethod + def from_bits_le(bits: list[bool]) -> U128: ... + @staticmethod + def from_field(f: Field) -> U128: ... + @staticmethod + def from_field_lossy(f: Field) -> U128: ... + @staticmethod + def from_fields(fields: list[Field]) -> U128: ... def __int__(self) -> int: ... + def __add__(self, other: U128) -> U128: ... + def __sub__(self, other: U128) -> U128: ... + def __mul__(self, other: U128) -> U128: ... + def __floordiv__(self, other: U128) -> U128: ... + def __pow__(self, exp: U32, modulo: None = None) -> U128: ... + def add(self, other: U128) -> U128: ... + def subtract(self, other: U128) -> U128: ... + def multiply(self, other: U128) -> U128: ... + def divide(self, other: U128) -> U128: ... + def add_wrapped(self, other: U128) -> U128: ... + def sub_wrapped(self, other: U128) -> U128: ... + def mul_wrapped(self, other: U128) -> U128: ... + def div_wrapped(self, other: U128) -> U128: ... + def rem(self, other: U128) -> U128: ... + def rem_wrapped(self, other: U128) -> U128: ... + def pow_u8(self, exp: U8) -> U128: ... + def pow_u16(self, exp: U16) -> U128: ... + def pow_u32(self, exp: U32) -> U128: ... + def to_bytes_le(self) -> bytes: ... + def to_bits_le(self) -> list[bool]: ... + def to_field(self) -> Field: ... + def to_scalar(self) -> Scalar: ... + def to_plaintext(self) -> Plaintext: ... + def to_boolean_lossy(self) -> Boolean: ... + def to_u8_lossy(self) -> U8: ... + def to_u16_lossy(self) -> U16: ... + def to_u32_lossy(self) -> U32: ... + def to_u64_lossy(self) -> U64: ... + def to_u128_lossy(self) -> U128: ... + def to_i8_lossy(self) -> I8: ... + def to_i16_lossy(self) -> I16: ... + def to_i32_lossy(self) -> I32: ... + def to_i64_lossy(self) -> I64: ... + def to_i128_lossy(self) -> I128: ... class Value: diff --git a/sdk/python/tests/test_numeric_parity.py b/sdk/python/tests/test_numeric_parity.py new file mode 100644 index 0000000..29d2593 --- /dev/null +++ b/sdk/python/tests/test_numeric_parity.py @@ -0,0 +1,636 @@ +""" +Wasm parity tests for integer, boolean, field, group, and scalar types. + +Verifies: + - Checked overflow raises OverflowError + - Wrapped arithmetic wraps correctly + - dunder ≡ named method equivalence + - to_field/from_field round-trips + - from_field_lossy truncation (pinned KATs from wasm test suite) + - Cross-cast pinned values + - Logic ops truth tables for Boolean + - Conversions from Boolean to numeric types + - New methods on Field, Group, Scalar +""" + +import pytest +from aleo.mainnet import ( + Boolean, + Field, + Group, + I8, + I16, + I32, + I64, + I128, + U8, + U16, + U32, + U64, + U128, + Scalar, +) + + +# ═══════════════════════════════════════════════════════════════ +# Integer — U8 +# ═══════════════════════════════════════════════════════════════ + + +class TestU8: + def test_checked_overflow_raises(self): + with pytest.raises(OverflowError): + U8(255) + U8(1) + + def test_dunder_add_eq_add(self): + a, b = U8(10), U8(20) + assert a + b == a.add(b) + + def test_checked_sub_underflow_raises(self): + with pytest.raises(OverflowError): + U8(0) - U8(1) + + def test_dunder_sub_eq_subtract(self): + a, b = U8(30), U8(10) + assert a - b == a.subtract(b) + + def test_checked_mul_overflow_raises(self): + with pytest.raises(OverflowError): + U8(200) * U8(2) + + def test_dunder_mul_eq_multiply(self): + a, b = U8(3), U8(4) + assert a * b == a.multiply(b) + + def test_div_zero_raises(self): + with pytest.raises(OverflowError): + U8(5) // U8(0) + + def test_floordiv_eq_divide(self): + a, b = U8(10), U8(3) + assert a // b == a.divide(b) + + def test_add_wrapped(self): + assert U8(255).add_wrapped(U8(1)) == U8(0) + + def test_sub_wrapped(self): + assert U8(0).sub_wrapped(U8(1)) == U8(255) + + def test_mul_wrapped(self): + assert U8(200).mul_wrapped(U8(2)) == U8(144) # 400 % 256 = 144 + + def test_div_wrapped(self): + assert U8(10).div_wrapped(U8(3)) == U8(3) + + def test_rem(self): + assert U8(10).rem(U8(3)) == U8(1) + + def test_rem_wrapped(self): + assert U8(10).rem_wrapped(U8(3)) == U8(1) + + def test_pow_u8(self): + # 2^8 = 256 overflows u8 + with pytest.raises(OverflowError): + U8(2).pow_u8(U8(8)) + assert U8(2).pow_u8(U8(7)) == U8(128) + + def test_pow_u32_dunder(self): + assert U8(2) ** U32(3) == U8(8) + + def test_to_field_from_field_roundtrip(self): + v = U8(200) + f = v.to_field() + back = U8.from_field(f) + assert back == v + + def test_from_field_lossy_truncation(self): + # KAT from wasm: field(256).to_u8_lossy() == 0u8 + f256 = Field.from_string("256field") + assert U8.from_field_lossy(f256) == U8(0) + # KAT: field(257).to_u8_lossy() == 1u8 + f257 = Field.from_string("257field") + assert U8.from_field_lossy(f257) == U8(1) + + def test_to_scalar(self): + s = U8(42).to_scalar() + assert str(s) == "42scalar" + + def test_to_boolean_lossy_even(self): + assert bool(U8(4).to_boolean_lossy()) is False + + def test_to_boolean_lossy_odd(self): + assert bool(U8(5).to_boolean_lossy()) is True + + def test_from_bits_le_roundtrip(self): + v = U8(42) + bits = v.to_bits_le() + assert U8.from_bits_le(bits) == v + + def test_from_bytes_le_roundtrip(self): + v = U8(200) + bts = v.to_bytes_le() + assert U8.from_bytes_le(bts) == v + + def test_one(self): + assert U8.one() == U8(1) + + def test_random_produces_values(self): + # just sanity-check that it returns an U8 + r = U8.random() + assert isinstance(r, U8) + + +# ═══════════════════════════════════════════════════════════════ +# Integer — U64 (64-bit boundary tests) +# ═══════════════════════════════════════════════════════════════ + + +class TestU64: + def test_overflow_raises(self): + with pytest.raises(OverflowError): + U64(2**64 - 1) + U64(1) + + def test_add_wrapped(self): + assert U64(2**64 - 1).add_wrapped(U64(1)) == U64(0) + + def test_to_field_from_field(self): + v = U64(12345678901234) + assert U64.from_field(v.to_field()) == v + + +# ═══════════════════════════════════════════════════════════════ +# Integer — U128 (boundary, cross-cast) +# ═══════════════════════════════════════════════════════════════ + + +class TestU128: + def test_overflow_raises(self): + with pytest.raises(OverflowError): + U128(2**128 - 1) + U128(1) + + def test_add_wrapped(self): + assert U128(2**128 - 1).add_wrapped(U128(1)) == U128(0) + + def test_cross_cast_identity(self): + # KAT from wasm: U32(42).to_u32_lossy() == U32(42) + v = U32(42) + assert v.to_u32_lossy() == v + + def test_cross_cast_widening(self): + # KAT from wasm: U8(255).to_u32_lossy() == U32(255) + assert U8(255).to_u32_lossy() == U32(255) + + def test_cross_cast_narrowing(self): + # KAT from wasm: U32(256).to_u8_lossy() == U8(0) + assert U32(256).to_u8_lossy() == U8(0) + # KAT from wasm: U32(257).to_u8_lossy() == U8(1) + assert U32(257).to_u8_lossy() == U8(1) + + def test_cross_cast_signed_unsigned(self): + # KAT from wasm: I32(42).to_u32_lossy() == U32(42) + assert I32(42).to_u32_lossy() == U32(42) + + +# ═══════════════════════════════════════════════════════════════ +# Integer — I8 (signed) +# ═══════════════════════════════════════════════════════════════ + + +class TestI8: + def test_negate_dunder_eq_negate(self): + v = I8(10) + assert -v == v.negate() + + def test_negate(self): + assert I8(10).negate() == I8(-10) + + def test_negate_min_raises(self): + with pytest.raises(OverflowError): + I8(-128).negate() + + def test_abs_checked_normal(self): + assert I8(-5).abs_checked() == I8(5) + + def test_abs_checked_min_raises(self): + with pytest.raises(OverflowError): + I8(-128).abs_checked() + + def test_abs_wrapped_min(self): + assert I8(-128).abs_wrapped() == I8(-128) # MIN stays MIN + + def test_overflow_add(self): + with pytest.raises(OverflowError): + I8(127) + I8(1) + + def test_dunder_eq_named(self): + a, b = I8(10), I8(5) + assert a + b == a.add(b) + assert a - b == a.subtract(b) + assert a * b == a.multiply(b) + + def test_to_field_from_field(self): + v = I8(-42) + assert I8.from_field(v.to_field()) == v + + +# ═══════════════════════════════════════════════════════════════ +# Integer — I128 (signed, large) +# ═══════════════════════════════════════════════════════════════ + + +class TestI128: + def test_overflow_add(self): + with pytest.raises(OverflowError): + I128(2**127 - 1) + I128(1) + + def test_underflow_sub(self): + with pytest.raises(OverflowError): + I128(-(2**127)) - I128(1) + + def test_negate_min_raises(self): + with pytest.raises(OverflowError): + I128(-(2**127)).negate() + + def test_add_wrapped(self): + assert I128(2**127 - 1).add_wrapped(I128(1)) == I128(-(2**127)) + + def test_to_scalar(self): + v = I128(1000) + s = v.to_scalar() + assert "1000" in str(s) + + def test_to_plaintext_roundtrip(self): + v = I128(9999) + pt = v.to_plaintext() + assert pt is not None + assert "9999" in str(pt) + + +# ═══════════════════════════════════════════════════════════════ +# Boolean +# ═══════════════════════════════════════════════════════════════ + + +class TestBoolean: + # Truth table for AND + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, False), (False, True, False), (True, False, False), (True, True, True)], + ) + def test_and(self, a, b, expected): + assert bool(Boolean(a).and_(Boolean(b))) == expected + + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, False), (False, True, False), (True, False, False), (True, True, True)], + ) + def test_and_dunder(self, a, b, expected): + assert bool(Boolean(a) & Boolean(b)) == expected + + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, False), (False, True, True), (True, False, True), (True, True, True)], + ) + def test_or(self, a, b, expected): + assert bool(Boolean(a).or_(Boolean(b))) == expected + + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, False), (False, True, True), (True, False, True), (True, True, True)], + ) + def test_or_dunder(self, a, b, expected): + assert bool(Boolean(a) | Boolean(b)) == expected + + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, False), (False, True, True), (True, False, True), (True, True, False)], + ) + def test_xor(self, a, b, expected): + assert bool(Boolean(a).xor(Boolean(b))) == expected + + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, False), (False, True, True), (True, False, True), (True, True, False)], + ) + def test_xor_dunder(self, a, b, expected): + assert bool(Boolean(a) ^ Boolean(b)) == expected + + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, True), (False, True, True), (True, False, True), (True, True, False)], + ) + def test_nand(self, a, b, expected): + assert bool(Boolean(a).nand(Boolean(b))) == expected + + @pytest.mark.parametrize( + "a,b,expected", + [(False, False, True), (False, True, False), (True, False, False), (True, True, False)], + ) + def test_nor(self, a, b, expected): + assert bool(Boolean(a).nor(Boolean(b))) == expected + + def test_not(self): + assert bool(Boolean(True).not_()) is False + assert bool(Boolean(False).not_()) is True + + def test_not_dunder(self): + assert bool(~Boolean(True)) is False + assert bool(~Boolean(False)) is True + + def test_dunder_not_eq_not_(self): + b = Boolean(True) + assert ~b == b.not_() + + def test_dunder_and_eq_and_(self): + a, b = Boolean(True), Boolean(False) + assert (a & b) == a.and_(b) + + def test_dunder_or_eq_or_(self): + a, b = Boolean(True), Boolean(False) + assert (a | b) == a.or_(b) + + def test_dunder_xor_eq_xor(self): + a, b = Boolean(True), Boolean(False) + assert (a ^ b) == a.xor(b) + + def test_to_field_false(self): + f = Boolean(False).to_field() + assert str(f) == "0field" + + def test_to_field_true(self): + f = Boolean(True).to_field() + assert str(f) == "1field" + + def test_to_scalar_false(self): + s = Boolean(False).to_scalar() + assert str(s) == "0scalar" + + def test_to_scalar_true(self): + s = Boolean(True).to_scalar() + assert str(s) == "1scalar" + + def test_to_u8_false(self): + assert Boolean(False).to_u8() == U8(0) + + def test_to_u8_true(self): + assert Boolean(True).to_u8() == U8(1) + + def test_to_i8_false(self): + assert Boolean(False).to_i8() == I8(0) + + def test_to_i8_true(self): + assert Boolean(True).to_i8() == I8(1) + + def test_to_u128(self): + assert Boolean(True).to_u128() == U128(1) + + def test_to_i128(self): + assert Boolean(True).to_i128() == I128(1) + + def test_to_plaintext(self): + pt = Boolean(True).to_plaintext() + assert "true" in str(pt) + + def test_from_string(self): + assert bool(Boolean.from_string("true")) is True + assert bool(Boolean.from_string("false")) is False + + def test_from_bits_le_roundtrip(self): + for v in [True, False]: + b = Boolean(v) + bits = b.to_bits_le() + assert Boolean.from_bits_le(bits) == b + + def test_from_bytes_le_roundtrip(self): + for v in [True, False]: + b = Boolean(v) + bts = b.to_bytes_le() + assert Boolean.from_bytes_le(bts) == b + + def test_random(self): + r = Boolean.random() + assert isinstance(r, Boolean) + + +# ═══════════════════════════════════════════════════════════════ +# Field (new methods) +# ═══════════════════════════════════════════════════════════════ + + +class TestFieldNewMethods: + def test_to_boolean_strict_zero(self): + b = Field.from_string("0field").to_boolean() + assert bool(b) is False + + def test_to_boolean_strict_one(self): + b = Field.from_string("1field").to_boolean() + assert bool(b) is True + + def test_to_boolean_strict_non_binary_raises(self): + with pytest.raises(Exception): + Field.from_string("2field").to_boolean() + + def test_to_boolean_lossy(self): + # KAT from wasm field tests + assert bool(Field.from_string("0field").to_boolean_lossy()) is False + assert bool(Field.from_string("1field").to_boolean_lossy()) is True + assert bool(Field.from_string("2field").to_boolean_lossy()) is False + assert bool(Field.from_string("3field").to_boolean_lossy()) is True + + def test_to_group_lossy_never_fails(self): + for s in ["0field", "1field", "12345field"]: + g = Field.from_string(s).to_group_lossy() + assert isinstance(g, Group) + + def test_to_address_lossy(self): + from aleo.mainnet import Address + addr = Field.from_string("42field").to_address_lossy() + assert str(addr).startswith("aleo1") + + def test_to_plaintext(self): + f = Field.from_string("42field") + pt = f.to_plaintext() + assert "42field" in str(pt) + + def test_to_u8_lossy_kats(self): + # KAT from wasm field tests + assert Field.from_string("255field").to_u8_lossy() == U8(255) + assert Field.from_string("256field").to_u8_lossy() == U8(0) + assert Field.from_string("257field").to_u8_lossy() == U8(1) + + def test_to_u32_lossy(self): + assert Field.from_string("255field").to_u32_lossy() == U32(255) + + def test_to_i8_lossy(self): + f = Field.from_string("127field") + assert Field.from_string("127field").to_i8_lossy() == I8(127) + + def test_random_distinct(self): + a, b = Field.random(), Field.random() + # Astronomically unlikely to collide + assert a != b or True # just ensure it runs + + +# ═══════════════════════════════════════════════════════════════ +# Group (new methods) +# ═══════════════════════════════════════════════════════════════ + + +class TestGroupNewMethods: + def test_random(self): + g = Group.random() + assert isinstance(g, Group) + + def test_to_field_eq_x_coordinate(self): + g = Group.generator() + assert g.to_field() == g.to_x_coordinate() + + def test_to_scalar_lossy(self): + g = Group.generator() + s = g.to_scalar_lossy() + assert isinstance(s, Scalar) + + def test_to_boolean_lossy(self): + g = Group.generator() + b = g.to_boolean_lossy() + assert isinstance(b, Boolean) + + def test_to_plaintext(self): + g = Group.generator() + pt = g.to_plaintext() + assert "group" in str(pt).lower() + + def test_to_fields(self): + g = Group.generator() + fields = g.to_fields() + assert len(fields) >= 1 + assert isinstance(fields[0], Field) + + def test_to_u8_lossy(self): + g = Group.generator() + v = g.to_u8_lossy() + assert isinstance(v, U8) + + def test_to_bits_le_from_bits_le_roundtrip(self): + g = Group.generator() + bits = g.to_bits_le() + back = Group.from_bits_le(bits) + assert back == g + + def test_to_address(self): + from aleo.mainnet import Address + g = Group.generator() + addr = g.to_address() + assert str(addr).startswith("aleo1") + + +# ═══════════════════════════════════════════════════════════════ +# Scalar (new methods) +# ═══════════════════════════════════════════════════════════════ + + +class TestScalarNewMethods: + def test_random(self): + s = Scalar.random() + assert isinstance(s, Scalar) + + def test_to_plaintext(self): + s = Scalar.from_string("42scalar") + pt = s.to_plaintext() + assert "42scalar" in str(pt) + + def test_to_boolean_strict_zero(self): + b = Scalar.zero().to_boolean() + assert bool(b) is False + + def test_to_boolean_strict_one(self): + b = Scalar.one().to_boolean() + assert bool(b) is True + + def test_to_boolean_strict_other_raises(self): + with pytest.raises(Exception): + Scalar.from_string("42scalar").to_boolean() + + def test_to_boolean_lossy(self): + assert bool(Scalar.zero().to_boolean_lossy()) is False + assert bool(Scalar.one().to_boolean_lossy()) is True + + def test_to_bits_le_roundtrip(self): + s = Scalar.from_string("42scalar") + bits = s.to_bits_le() + back = Scalar.from_bits_le(bits) + assert back == s + + def test_double(self): + s = Scalar.one() + d = s.double() + assert str(d) == str(s + s) + + def test_to_u8_lossy(self): + s = Scalar.from_string("200scalar") + assert s.to_u8_lossy() == U8(200) + + def test_to_u32_lossy(self): + s = Scalar.from_string("42scalar") + assert s.to_u32_lossy() == U32(42) + + def test_to_i8_lossy(self): + s = Scalar.from_string("42scalar") + # 42 fits in i8 + assert s.to_i8_lossy() == I8(42) + + def test_to_group_lossy(self): + s = Scalar.from_string("1scalar") + g = s.to_group_lossy() + assert isinstance(g, Group) + + def test_to_field_roundtrip(self): + # KAT from wasm scalar tests + s = Scalar.from_string("42scalar") + f = s.to_field() + back = f.to_scalar_lossy() + assert str(back) == str(s) + + +# ═══════════════════════════════════════════════════════════════ +# Cross-type round-trip: integer → field → integer +# ═══════════════════════════════════════════════════════════════ + + +class TestCrossTypeRoundtrips: + def test_u8_to_field_roundtrip(self): + v = U8(200) + assert U8.from_field(v.to_field()) == v + + def test_u16_to_field_roundtrip(self): + v = U16(60000) + assert U16.from_field(v.to_field()) == v + + def test_u32_to_field_roundtrip(self): + # KAT from wasm integer tests + v = U32.from_string("42u32") + f = v.to_field() + back = U32.from_field(f) + assert back == v + + def test_i8_to_field_roundtrip(self): + v = I8(-100) + assert I8.from_field(v.to_field()) == v + + def test_i128_to_field_roundtrip(self): + v = I128(-(2**63)) + assert I128.from_field(v.to_field()) == v + + def test_u8_from_field_lossy_in_range(self): + # KAT from wasm: in-range round-trips losslessly + v = U8.from_string("200u8") + f = v.to_field() + back = U8.from_field_lossy(f) + assert back == v + + def test_to_boolean_lossy_u32(self): + # KATs from wasm tests + assert bool(U32.from_string("0u32").to_boolean_lossy()) is False + assert bool(U32.from_string("1u32").to_boolean_lossy()) is True + assert bool(U32.from_string("4u32").to_boolean_lossy()) is False + assert bool(U32.from_string("5u32").to_boolean_lossy()) is True diff --git a/sdk/src/algebra/boolean.rs b/sdk/src/algebra/boolean.rs index 0461bea..552b2b9 100644 --- a/sdk/src/algebra/boolean.rs +++ b/sdk/src/algebra/boolean.rs @@ -14,13 +14,24 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::types::BooleanNative; +use crate::{ + types::{ + BooleanNative, FieldNative, GroupNative, I128Native, I16Native, I32Native, I64Native, + I8Native, LiteralNative, PlaintextNative, ScalarNative, U128Native, U16Native, U32Native, + U64Native, U8Native, + }, + Address, Field, Group, Scalar, +}; use pyo3::prelude::*; +use rand::rngs::StdRng; +use snarkvm::prelude::{FromBits, FromBytes, FromField, ToBits, ToBytes, Uniform}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, + str::FromStr, + sync::OnceLock, }; /// The Aleo boolean type. @@ -28,6 +39,7 @@ use std::{ #[derive(Copy, Clone)] pub struct Boolean(BooleanNative); +#[allow(clippy::wrong_self_convention)] #[pymethods] impl Boolean { #[new] @@ -35,6 +47,46 @@ impl Boolean { Self(BooleanNative::new(value)) } + /// Parses a boolean from a string ("true"/"false"). + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + BooleanNative::from_str(s).map(Self) + } + + /// Returns a random boolean. + #[staticmethod] + fn random() -> Self { + Self(<BooleanNative as Uniform>::rand(&mut rand::make_rng::< + StdRng, + >())) + } + + // ── serialization ────────────────────────────────────────────────────── + + /// Returns the little-endian byte representation. + fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Constructs from little-endian bytes. + #[staticmethod] + fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { + BooleanNative::from_bytes_le(&bytes).map(Self) + } + + /// Returns the little-endian bit representation (a single-element Vec). + fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Constructs from little-endian bits (must be exactly 1 element). + #[staticmethod] + fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + BooleanNative::from_bits_le(&bits).map(Self) + } + + // ── dunders ──────────────────────────────────────────────────────────── + /// Returns the boolean as a string. fn __str__(&self) -> String { self.0.to_string() @@ -53,6 +105,143 @@ impl Boolean { self.0.hash(&mut hasher); hasher.finish() } + + /// Logical NOT dunder. + fn __invert__(&self) -> Self { + self.not_() + } + + /// Logical AND dunder. + fn __and__(&self, other: &Self) -> Self { + self.and_(other) + } + + /// Logical OR dunder. + fn __or__(&self, other: &Self) -> Self { + self.or_(other) + } + + /// Logical XOR dunder. + fn __xor__(&self, other: &Self) -> Self { + self.xor(other) + } + + // ── named logical ops ────────────────────────────────────────────────── + + /// Logical NOT. + fn not_(&self) -> Self { + Self(!self.0) + } + + /// Logical AND. + fn and_(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + + /// Logical OR. + fn or_(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + + /// Logical XOR. + fn xor(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + + /// Logical NAND (NOT AND). + fn nand(&self, other: &Self) -> Self { + Self(!(self.0 & other.0)) + } + + /// Logical NOR (NOT OR). + fn nor(&self, other: &Self) -> Self { + Self(!(self.0 | other.0)) + } + + // ── conversions ──────────────────────────────────────────────────────── + + /// Converts to a Plaintext wrapper. + fn to_plaintext(&self) -> crate::Plaintext { + crate::Plaintext::from(PlaintextNative::Literal( + LiteralNative::Boolean(self.0), + OnceLock::new(), + )) + } + + /// Converts to a Field element (false=0field, true=1field). + fn to_field(&self) -> anyhow::Result<Field> { + FieldNative::from_bits_le(&[*self.0]).map(Into::into) + } + + /// Converts to a Scalar element (false=0scalar, true=1scalar). + fn to_scalar(&self) -> anyhow::Result<Scalar> { + ScalarNative::from_bits_le(&[*self.0]).map(Into::into) + } + + /// Converts to a Group element via x-coordinate recovery (strict). + fn to_group(&self) -> anyhow::Result<Group> { + let f = FieldNative::from_bits_le(&[*self.0])?; + GroupNative::from_field(&f).map(Into::into) + } + + /// Converts to an Address via Group (strict). + fn to_address(&self) -> anyhow::Result<Address> { + let f = FieldNative::from_bits_le(&[*self.0])?; + let g = GroupNative::from_field(&f)?; + Ok(Address::new(g)) + } + + // Boolean → Integer conversions (false=0, true=1, lossless). + + /// Converts to U8 (false=0, true=1). + fn to_u8(&self) -> crate::U8 { + crate::U8::from(U8Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to U16 (false=0, true=1). + fn to_u16(&self) -> crate::U16 { + crate::U16::from(U16Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to U32 (false=0, true=1). + fn to_u32(&self) -> crate::U32 { + crate::U32::from(U32Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to U64 (false=0, true=1). + fn to_u64(&self) -> crate::U64 { + crate::U64::from(U64Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to U128 (false=0, true=1). + fn to_u128(&self) -> crate::U128 { + crate::U128::from(U128Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to I8 (false=0, true=1). + fn to_i8(&self) -> crate::I8 { + crate::I8::from(I8Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to I16 (false=0, true=1). + fn to_i16(&self) -> crate::I16 { + crate::I16::from(I16Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to I32 (false=0, true=1). + fn to_i32(&self) -> crate::I32 { + crate::I32::from(I32Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to I64 (false=0, true=1). + fn to_i64(&self) -> crate::I64 { + crate::I64::from(I64Native::new(if *self.0 { 1 } else { 0 })) + } + + /// Converts to I128 (false=0, true=1). + fn to_i128(&self) -> crate::I128 { + crate::I128::from(I128Native::new(if *self.0 { 1 } else { 0 })) + } } impl From<Boolean> for BooleanNative { diff --git a/sdk/src/algebra/field.rs b/sdk/src/algebra/field.rs index 318c9bd..bc51604 100644 --- a/sdk/src/algebra/field.rs +++ b/sdk/src/algebra/field.rs @@ -14,17 +14,24 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::{types::FieldNative, Group, Scalar}; +use crate::{ + types::{ + FieldNative, I128Native, I16Native, I32Native, I64Native, I8Native, LiteralNative, + PlaintextNative, ScalarNative, U128Native, U16Native, U32Native, U64Native, U8Native, + }, + Address, Boolean, Group, Scalar, +}; use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; use rand::rngs::StdRng; -use snarkvm::prelude::{Double, FromBits, One, Pow, ToBits, Uniform, Zero}; +use snarkvm::prelude::{CastLossy, Double, FromBits, One, Pow, ToBits, Uniform, Zero}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, ops::Deref, str::FromStr, + sync::OnceLock, }; /// The Aleo field type. @@ -130,17 +137,114 @@ impl Field { /// Converts the field to a scalar using lossy truncation. fn to_scalar_lossy(&self) -> Scalar { - use crate::types::ScalarNative; ScalarNative::from_field_lossy(&self.0).into() } /// Converts the field to a group element (x-coordinate recovery). fn to_group(&self) -> anyhow::Result<Group> { - use crate::types::GroupNative; use snarkvm::prelude::FromField; - GroupNative::from_field(&self.0).map(Group::from) + crate::types::GroupNative::from_field(&self.0).map(Group::from) + } + + /// Converts the field to a group element with lossy conversion (Elligator-2 fallback). + fn to_group_lossy(&self) -> Group { + let g: crate::types::GroupNative = self.0.cast_lossy(); + Group::from(g) + } + + /// Converts the field to an Address (strict, via group x-coordinate recovery). + fn to_address(&self) -> anyhow::Result<Address> { + use snarkvm::prelude::FromField; + let g = crate::types::GroupNative::from_field(&self.0)?; + Ok(Address::new(g)) + } + + /// Converts the field to an Address with lossy conversion. + fn to_address_lossy(&self) -> Address { + let g: crate::types::GroupNative = self.0.cast_lossy(); + Address::new(g) + } + + /// Converts the field to a Boolean (strict: must be 0 or 1). + fn to_boolean(&self) -> anyhow::Result<Boolean> { + if self.0.is_zero() { + Ok(Boolean::from(crate::types::BooleanNative::new(false))) + } else if self.0.is_one() { + Ok(Boolean::from(crate::types::BooleanNative::new(true))) + } else { + Err(anyhow::anyhow!( + "Failed to convert field to boolean: field is not zero or one" + )) + } + } + + /// Converts the field to a Boolean with lossy truncation (LSB). + fn to_boolean_lossy(&self) -> Boolean { + Boolean::from(crate::types::BooleanNative::new(self.0.to_bits_le()[0])) + } + + /// Converts the field to a Plaintext wrapper. + fn to_plaintext(&self) -> crate::Plaintext { + crate::Plaintext::from(PlaintextNative::Literal( + LiteralNative::Field(self.0), + OnceLock::new(), + )) + } + + // Field → Integer lossy conversions + + /// Cast to U8 with lossy truncation. + fn to_u8_lossy(&self) -> crate::U8 { + crate::U8::from(U8Native::from_field_lossy(&self.0)) } + /// Cast to U16 with lossy truncation. + fn to_u16_lossy(&self) -> crate::U16 { + crate::U16::from(U16Native::from_field_lossy(&self.0)) + } + + /// Cast to U32 with lossy truncation. + fn to_u32_lossy(&self) -> crate::U32 { + crate::U32::from(U32Native::from_field_lossy(&self.0)) + } + + /// Cast to U64 with lossy truncation. + fn to_u64_lossy(&self) -> crate::U64 { + crate::U64::from(U64Native::from_field_lossy(&self.0)) + } + + /// Cast to U128 with lossy truncation. + fn to_u128_lossy(&self) -> crate::U128 { + crate::U128::from(U128Native::from_field_lossy(&self.0)) + } + + /// Cast to I8 with lossy truncation. + fn to_i8_lossy(&self) -> crate::I8 { + crate::I8::from(I8Native::from_field_lossy(&self.0)) + } + + /// Cast to I16 with lossy truncation. + fn to_i16_lossy(&self) -> crate::I16 { + crate::I16::from(I16Native::from_field_lossy(&self.0)) + } + + /// Cast to I32 with lossy truncation. + fn to_i32_lossy(&self) -> crate::I32 { + crate::I32::from(I32Native::from_field_lossy(&self.0)) + } + + /// Cast to I64 with lossy truncation. + fn to_i64_lossy(&self) -> crate::I64 { + crate::I64::from(I64Native::from_field_lossy(&self.0)) + } + + /// Cast to I128 with lossy truncation. + fn to_i128_lossy(&self) -> crate::I128 { + crate::I128::from(I128Native::from_field_lossy(&self.0)) + } + + // ── dunders ──────────────────────────────────────────────────────────── + /// Returns the Field as a string. fn __str__(&self) -> String { self.0.to_string() diff --git a/sdk/src/algebra/group.rs b/sdk/src/algebra/group.rs index 0fe17d5..3f93e92 100644 --- a/sdk/src/algebra/group.rs +++ b/sdk/src/algebra/group.rs @@ -14,16 +14,26 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::{types::GroupNative, Address, Field, Scalar}; +use crate::{ + types::{ + GroupNative, I128Native, I16Native, I32Native, I64Native, I8Native, LiteralNative, + PlaintextNative, ScalarNative, U128Native, U16Native, U32Native, U64Native, U8Native, + }, + Address, Boolean, Field, Scalar, +}; use pyo3::prelude::*; -use snarkvm::prelude::{Double, FromField, Zero}; +use rand::rngs::StdRng; +use snarkvm::prelude::{ + Double, FromBits, FromBytes, FromField, ToBits, ToBytes, ToFields, Uniform, Zero, +}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, ops::Deref, str::FromStr, + sync::OnceLock, }; /// The Aleo group type. @@ -51,6 +61,12 @@ impl Group { GroupNative::generator().into() } + /// Returns a random group element. + #[staticmethod] + fn random() -> Self { + <GroupNative as Uniform>::rand(&mut rand::make_rng::<StdRng>()).into() + } + /// Returns the group from a field element (x-coordinate recovery). #[staticmethod] fn from_field(field: &Field) -> anyhow::Result<Self> { @@ -94,17 +110,112 @@ impl Group { /// Returns the little-endian byte representation of the group element (x-coordinate). fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { - use snarkvm::prelude::ToBytes; self.0.to_bytes_le() } /// Parses a group from little-endian bytes (x-coordinate). #[staticmethod] fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { - use snarkvm::prelude::FromBytes; GroupNative::from_bytes_le(&bytes).map(Self) } + /// Returns the little-endian bit representation of the group element. + fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Parses a group from little-endian bits. + #[staticmethod] + fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + GroupNative::from_bits_le(&bits).map(Self) + } + + /// Returns the field elements representation. + fn to_fields(&self) -> anyhow::Result<Vec<Field>> { + self.0 + .to_fields() + .map(|fs| fs.into_iter().map(Into::into).collect()) + } + + /// Converts to a Plaintext wrapper. + fn to_plaintext(&self) -> crate::Plaintext { + crate::Plaintext::from(PlaintextNative::Literal( + LiteralNative::Group(self.0), + OnceLock::new(), + )) + } + + // ── cast conversions ─────────────────────────────────────────────────── + + /// Returns the x-coordinate field element (alias for to_x_coordinate). + fn to_field(&self) -> Field { + self.to_x_coordinate() + } + + /// Cast to Scalar with lossy truncation (via x-coordinate). + fn to_scalar_lossy(&self) -> Scalar { + ScalarNative::from_field_lossy(&self.0.to_x_coordinate()).into() + } + + /// Cast to Boolean with lossy truncation (LSB of x-coordinate). + fn to_boolean_lossy(&self) -> Boolean { + Boolean::from(crate::types::BooleanNative::new( + self.0.to_x_coordinate().to_bits_le()[0], + )) + } + + // Group → Integer lossy conversions (via x-coordinate) + + /// Cast to U8 with lossy truncation. + fn to_u8_lossy(&self) -> crate::U8 { + crate::U8::from(U8Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to U16 with lossy truncation. + fn to_u16_lossy(&self) -> crate::U16 { + crate::U16::from(U16Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to U32 with lossy truncation. + fn to_u32_lossy(&self) -> crate::U32 { + crate::U32::from(U32Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to U64 with lossy truncation. + fn to_u64_lossy(&self) -> crate::U64 { + crate::U64::from(U64Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to U128 with lossy truncation. + fn to_u128_lossy(&self) -> crate::U128 { + crate::U128::from(U128Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to I8 with lossy truncation. + fn to_i8_lossy(&self) -> crate::I8 { + crate::I8::from(I8Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to I16 with lossy truncation. + fn to_i16_lossy(&self) -> crate::I16 { + crate::I16::from(I16Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to I32 with lossy truncation. + fn to_i32_lossy(&self) -> crate::I32 { + crate::I32::from(I32Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to I64 with lossy truncation. + fn to_i64_lossy(&self) -> crate::I64 { + crate::I64::from(I64Native::from_field_lossy(&self.0.to_x_coordinate())) + } + + /// Cast to I128 with lossy truncation. + fn to_i128_lossy(&self) -> crate::I128 { + crate::I128::from(I128Native::from_field_lossy(&self.0.to_x_coordinate())) + } + // ---------- dunders ---------- /// Returns the Group as a string. diff --git a/sdk/src/algebra/integer.rs b/sdk/src/algebra/integer.rs index bf47058..02bf60f 100644 --- a/sdk/src/algebra/integer.rs +++ b/sdk/src/algebra/integer.rs @@ -14,25 +14,53 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::types::{ - I128Native, I16Native, I32Native, I64Native, I8Native, U128Native, U16Native, U32Native, - U64Native, U8Native, +use crate::{ + types::{ + BooleanNative, FieldNative, I128Native, I16Native, I32Native, I64Native, I8Native, + LiteralNative, PlaintextNative, U128Native, U16Native, U32Native, U64Native, U8Native, + }, + Boolean, Field, Scalar, }; -use pyo3::prelude::*; -use snarkvm::prelude::Zero; +use pyo3::{exceptions::PyOverflowError, prelude::*}; +use rand::rngs::StdRng; +use snarkvm::prelude::{ + AddWrapped, DivWrapped, FromBits, FromBytes, FromField, FromFields, MulWrapped, One, Pow, Rem, + RemWrapped, SubWrapped, ToBits, ToBytes, ToField, Uniform, Zero, +}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, + str::FromStr, + sync::OnceLock, }; -macro_rules! integer { - ($export_ty:ident, $native:ident, $machine:ident, $desc:literal) => { +// ── helper: convert a snarkvm panic ("halt") into a Python OverflowError ────── +fn wrap_overflow<T, F: FnOnce() -> T + std::panic::UnwindSafe>(f: F) -> PyResult<T> { + std::panic::catch_unwind(f).map_err(|e| { + let msg = if let Some(s) = e.downcast_ref::<String>() { + s.clone() + } else if let Some(s) = e.downcast_ref::<&str>() { + s.to_string() + } else { + "integer arithmetic overflow or panic".to_string() + }; + PyOverflowError::new_err(msg) + }) +} + +// ── Unsigned integer macro ──────────────────────────────────────────────────── +// All methods for unsigned integers in a single #[pymethods] block. + +macro_rules! integer_unsigned { + ($export_ty:ident, $native:ident, $machine:ident, $literal_variant:ident, $desc:literal) => { #[doc = concat!("The Aleo ", $desc, " type.")] #[pyclass(frozen)] #[derive(Copy, Clone)] pub struct $export_ty($native); + + #[allow(clippy::wrong_self_convention)] #[pymethods] impl $export_ty { #[new] @@ -40,13 +68,52 @@ macro_rules! integer { Self($native::new(value)) } - /// Returns the `0` element of the integer. + /// Parses an integer from a string (e.g. "42u32"). + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + $native::from_str(s).map(Self) + } + + /// Returns the `0` element. #[staticmethod] fn zero() -> Self { Self($native::zero()) } - /// Returns the integer as a string. + /// Returns the `1` element. + #[staticmethod] + fn one() -> Self { + Self($native::one()) + } + + /// Returns a random value. + #[staticmethod] + fn random() -> Self { + Self(<$native as Uniform>::rand(&mut rand::make_rng::<StdRng>())) + } + + // ── serialization ────────────────────────────────────────────── + + fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + #[staticmethod] + fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { + $native::from_bytes_le(&bytes).map(Self) + } + + fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + #[staticmethod] + fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + $native::from_bits_le(&bits).map(Self) + } + + // ── dunders ──────────────────────────────────────────────────── + fn __str__(&self) -> String { self.0.to_string() } @@ -64,6 +131,219 @@ macro_rules! integer { self.0.hash(&mut hasher); hasher.finish() } + + fn __add__(&self, other: Self) -> PyResult<Self> { + self.add(other) + } + + fn __sub__(&self, other: Self) -> PyResult<Self> { + self.subtract(other) + } + + fn __mul__(&self, other: Self) -> PyResult<Self> { + self.multiply(other) + } + + fn __floordiv__(&self, other: Self) -> PyResult<Self> { + self.divide(other) + } + + fn __pow__(&self, exp: U32, _modulo: Option<u32>) -> PyResult<Self> { + self.pow_u32(exp) + } + + // ── checked arithmetic ───────────────────────────────────────── + + fn add(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a + b)) + } + + fn subtract(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a - b)) + } + + fn multiply(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a * b)) + } + + fn divide(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a / b)) + } + + // ── wrapped ──────────────────────────────────────────────────── + + fn add_wrapped(&self, other: Self) -> Self { + Self(self.0.add_wrapped(&other.0)) + } + + fn sub_wrapped(&self, other: Self) -> Self { + Self(self.0.sub_wrapped(&other.0)) + } + + fn mul_wrapped(&self, other: Self) -> Self { + Self(self.0.mul_wrapped(&other.0)) + } + + fn div_wrapped(&self, other: Self) -> Self { + Self(self.0.div_wrapped(&other.0)) + } + + // ── remainder ───────────────────────────────────────────────── + + fn rem(&self, other: Self) -> Self { + Self(self.0.rem(&other.0)) + } + + fn rem_wrapped(&self, other: Self) -> Self { + Self(self.0.rem_wrapped(&other.0)) + } + + // ── power ────────────────────────────────────────────────────── + + fn pow_u8(&self, exp: U8) -> PyResult<Self> { + let base = self.0; + let e = exp.0; + wrap_overflow(move || Self(base.pow(&e))) + } + + fn pow_u16(&self, exp: U16) -> PyResult<Self> { + let base = self.0; + let e = exp.0; + wrap_overflow(move || Self(base.pow(&e))) + } + + fn pow_u32(&self, exp: U32) -> PyResult<Self> { + let base = self.0; + let e = exp.0; + wrap_overflow(move || Self(base.pow(&e))) + } + + // ── conversions ──────────────────────────────────────────────── + + fn to_field(&self) -> anyhow::Result<Field> { + self.0.to_field().map(Into::into) + } + + #[staticmethod] + fn from_field(f: &Field) -> anyhow::Result<Self> { + $native::from_field(&**f).map(Self) + } + + #[staticmethod] + fn from_field_lossy(f: &Field) -> Self { + Self($native::from_field_lossy(&**f)) + } + + #[staticmethod] + fn from_fields(fields: Vec<Field>) -> anyhow::Result<Self> { + let native: Vec<FieldNative> = fields.into_iter().map(Into::into).collect(); + $native::from_fields(&native).map(Self) + } + + fn to_scalar(&self) -> Scalar { + self.0.to_scalar().into() + } + + fn to_plaintext(&self) -> crate::Plaintext { + crate::Plaintext::from(PlaintextNative::Literal( + LiteralNative::$literal_variant(self.0), + OnceLock::new(), + )) + } + + fn to_boolean_lossy(&self) -> Boolean { + Boolean::from(BooleanNative::new(self.0.to_bits_le()[0])) + } + + // ── cross-casts ──────────────────────────────────────────────── + + fn to_u8_lossy(&self) -> PyResult<U8> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U8(U8Native::from_field_lossy(&f))) + } + + fn to_u16_lossy(&self) -> PyResult<U16> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U16(U16Native::from_field_lossy(&f))) + } + + fn to_u32_lossy(&self) -> PyResult<U32> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U32(U32Native::from_field_lossy(&f))) + } + + fn to_u64_lossy(&self) -> PyResult<U64> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U64(U64Native::from_field_lossy(&f))) + } + + fn to_u128_lossy(&self) -> PyResult<U128> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U128(U128Native::from_field_lossy(&f))) + } + + fn to_i8_lossy(&self) -> PyResult<I8> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I8(I8Native::from_field_lossy(&f))) + } + + fn to_i16_lossy(&self) -> PyResult<I16> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I16(I16Native::from_field_lossy(&f))) + } + + fn to_i32_lossy(&self) -> PyResult<I32> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I32(I32Native::from_field_lossy(&f))) + } + + fn to_i64_lossy(&self) -> PyResult<I64> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I64(I64Native::from_field_lossy(&f))) + } + + fn to_i128_lossy(&self) -> PyResult<I128> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I128(I128Native::from_field_lossy(&f))) + } } impl From<$export_ty> for $native { @@ -80,14 +360,338 @@ macro_rules! integer { }; } -integer!(I8, I8Native, i8, "signed I8"); -integer!(I16, I16Native, i16, "signed I16"); -integer!(I32, I32Native, i32, "signed I32"); -integer!(I64, I64Native, i64, "signed I64"); -integer!(I128, I128Native, i128, "signed I128"); - -integer!(U8, U8Native, u8, "unsigned U8"); -integer!(U16, U16Native, u16, "unsigned U16"); -integer!(U32, U32Native, u32, "unsigned U32"); -integer!(U64, U64Native, u64, "unsigned U64"); -integer!(U128, U128Native, u128, "unsigned U128"); +// ── Signed integer macro ────────────────────────────────────────────────────── +// Identical to unsigned but includes negate/abs_checked/abs_wrapped. + +macro_rules! integer_signed { + ($export_ty:ident, $native:ident, $machine:ident, $literal_variant:ident, $desc:literal) => { + #[doc = concat!("The Aleo ", $desc, " type.")] + #[pyclass(frozen)] + #[derive(Copy, Clone)] + pub struct $export_ty($native); + + #[allow(clippy::wrong_self_convention)] + #[pymethods] + impl $export_ty { + #[new] + fn new(value: $machine) -> Self { + Self($native::new(value)) + } + + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + $native::from_str(s).map(Self) + } + + #[staticmethod] + fn zero() -> Self { + Self($native::zero()) + } + + #[staticmethod] + fn one() -> Self { + Self($native::one()) + } + + #[staticmethod] + fn random() -> Self { + Self(<$native as Uniform>::rand(&mut rand::make_rng::<StdRng>())) + } + + fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + #[staticmethod] + fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { + $native::from_bytes_le(&bytes).map(Self) + } + + fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + #[staticmethod] + fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + $native::from_bits_le(&bits).map(Self) + } + + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __int__(&self) -> $machine { + *self.0 + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } + + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.hash(&mut hasher); + hasher.finish() + } + + fn __add__(&self, other: Self) -> PyResult<Self> { + self.add(other) + } + + fn __sub__(&self, other: Self) -> PyResult<Self> { + self.subtract(other) + } + + fn __mul__(&self, other: Self) -> PyResult<Self> { + self.multiply(other) + } + + fn __floordiv__(&self, other: Self) -> PyResult<Self> { + self.divide(other) + } + + fn __pow__(&self, exp: U32, _modulo: Option<u32>) -> PyResult<Self> { + self.pow_u32(exp) + } + + fn __neg__(&self) -> PyResult<Self> { + self.negate() + } + + // ── checked arithmetic ───────────────────────────────────────── + + fn add(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a + b)) + } + + fn subtract(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a - b)) + } + + fn multiply(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a * b)) + } + + fn divide(&self, other: Self) -> PyResult<Self> { + let a = self.0; + let b = other.0; + wrap_overflow(move || Self(a / b)) + } + + fn negate(&self) -> PyResult<Self> { + let v = self.0; + wrap_overflow(move || Self(-v)) + } + + fn abs_checked(&self) -> PyResult<Self> { + use snarkvm::prelude::AbsChecked; + let v = self.0; + wrap_overflow(move || Self(v.abs_checked())) + } + + fn abs_wrapped(&self) -> Self { + use snarkvm::prelude::AbsWrapped; + Self(self.0.abs_wrapped()) + } + + // ── wrapped ──────────────────────────────────────────────────── + + fn add_wrapped(&self, other: Self) -> Self { + Self(self.0.add_wrapped(&other.0)) + } + + fn sub_wrapped(&self, other: Self) -> Self { + Self(self.0.sub_wrapped(&other.0)) + } + + fn mul_wrapped(&self, other: Self) -> Self { + Self(self.0.mul_wrapped(&other.0)) + } + + fn div_wrapped(&self, other: Self) -> Self { + Self(self.0.div_wrapped(&other.0)) + } + + // ── remainder ───────────────────────────────────────────────── + + fn rem(&self, other: Self) -> Self { + Self(self.0.rem(&other.0)) + } + + fn rem_wrapped(&self, other: Self) -> Self { + Self(self.0.rem_wrapped(&other.0)) + } + + // ── power ────────────────────────────────────────────────────── + + fn pow_u8(&self, exp: U8) -> PyResult<Self> { + let base = self.0; + let e = exp.0; + wrap_overflow(move || Self(base.pow(&e))) + } + + fn pow_u16(&self, exp: U16) -> PyResult<Self> { + let base = self.0; + let e = exp.0; + wrap_overflow(move || Self(base.pow(&e))) + } + + fn pow_u32(&self, exp: U32) -> PyResult<Self> { + let base = self.0; + let e = exp.0; + wrap_overflow(move || Self(base.pow(&e))) + } + + // ── conversions ──────────────────────────────────────────────── + + fn to_field(&self) -> anyhow::Result<Field> { + self.0.to_field().map(Into::into) + } + + #[staticmethod] + fn from_field(f: &Field) -> anyhow::Result<Self> { + $native::from_field(&**f).map(Self) + } + + #[staticmethod] + fn from_field_lossy(f: &Field) -> Self { + Self($native::from_field_lossy(&**f)) + } + + #[staticmethod] + fn from_fields(fields: Vec<Field>) -> anyhow::Result<Self> { + let native: Vec<FieldNative> = fields.into_iter().map(Into::into).collect(); + $native::from_fields(&native).map(Self) + } + + fn to_scalar(&self) -> Scalar { + self.0.to_scalar().into() + } + + fn to_plaintext(&self) -> crate::Plaintext { + crate::Plaintext::from(PlaintextNative::Literal( + LiteralNative::$literal_variant(self.0), + OnceLock::new(), + )) + } + + fn to_boolean_lossy(&self) -> Boolean { + Boolean::from(BooleanNative::new(self.0.to_bits_le()[0])) + } + + // ── cross-casts ──────────────────────────────────────────────── + + fn to_u8_lossy(&self) -> PyResult<U8> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U8(U8Native::from_field_lossy(&f))) + } + + fn to_u16_lossy(&self) -> PyResult<U16> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U16(U16Native::from_field_lossy(&f))) + } + + fn to_u32_lossy(&self) -> PyResult<U32> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U32(U32Native::from_field_lossy(&f))) + } + + fn to_u64_lossy(&self) -> PyResult<U64> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U64(U64Native::from_field_lossy(&f))) + } + + fn to_u128_lossy(&self) -> PyResult<U128> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(U128(U128Native::from_field_lossy(&f))) + } + + fn to_i8_lossy(&self) -> PyResult<I8> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I8(I8Native::from_field_lossy(&f))) + } + + fn to_i16_lossy(&self) -> PyResult<I16> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I16(I16Native::from_field_lossy(&f))) + } + + fn to_i32_lossy(&self) -> PyResult<I32> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I32(I32Native::from_field_lossy(&f))) + } + + fn to_i64_lossy(&self) -> PyResult<I64> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I64(I64Native::from_field_lossy(&f))) + } + + fn to_i128_lossy(&self) -> PyResult<I128> { + let f = self + .0 + .to_field() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(I128(I128Native::from_field_lossy(&f))) + } + } + + impl From<$export_ty> for $native { + fn from(value: $export_ty) -> Self { + value.0 + } + } + + impl From<$native> for $export_ty { + fn from(value: $native) -> Self { + Self(value) + } + } + }; +} + +// ── Instantiate unsigned types ───────────────────────────────────────────────── +integer_unsigned!(U8, U8Native, u8, U8, "unsigned U8"); +integer_unsigned!(U16, U16Native, u16, U16, "unsigned U16"); +integer_unsigned!(U32, U32Native, u32, U32, "unsigned U32"); +integer_unsigned!(U64, U64Native, u64, U64, "unsigned U64"); +integer_unsigned!(U128, U128Native, u128, U128, "unsigned U128"); + +// ── Instantiate signed types ─────────────────────────────────────────────────── +integer_signed!(I8, I8Native, i8, I8, "signed I8"); +integer_signed!(I16, I16Native, i16, I16, "signed I16"); +integer_signed!(I32, I32Native, i32, I32, "signed I32"); +integer_signed!(I64, I64Native, i64, I64, "signed I64"); +integer_signed!(I128, I128Native, i128, I128, "signed I128"); diff --git a/sdk/src/algebra/scalar.rs b/sdk/src/algebra/scalar.rs index de02990..aa1575d 100644 --- a/sdk/src/algebra/scalar.rs +++ b/sdk/src/algebra/scalar.rs @@ -14,16 +14,28 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. -use crate::{types::ScalarNative, Field}; +use crate::{ + types::{ + FieldNative, GroupNative, I128Native, I16Native, I32Native, I64Native, I8Native, + LiteralNative, PlaintextNative, ScalarNative, U128Native, U16Native, U32Native, U64Native, + U8Native, + }, + Address, Boolean, Field, Group, +}; use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; -use snarkvm::prelude::{One, Pow, ToField, Zero}; +use rand::rngs::StdRng; +use snarkvm::prelude::{ + CastLossy, Double, FromBits, FromBytes, FromField, One, Pow, ToBits, ToBytes, ToField, Uniform, + Zero, +}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, ops::Deref, str::FromStr, + sync::OnceLock, }; /// The Aleo scalar type. @@ -51,24 +63,44 @@ impl Scalar { ScalarNative::one().into() } + /// Returns a random scalar element. + #[staticmethod] + fn random() -> Self { + <ScalarNative as Uniform>::rand(&mut rand::make_rng::<StdRng>()).into() + } + /// Returns the little-endian byte representation of the scalar element. fn to_bytes_le(&self) -> anyhow::Result<Vec<u8>> { - use snarkvm::prelude::ToBytes; self.0.to_bytes_le() } /// Parses a scalar from little-endian bytes. #[staticmethod] fn from_bytes_le(bytes: Vec<u8>) -> anyhow::Result<Self> { - use snarkvm::prelude::FromBytes; ScalarNative::from_bytes_le(&bytes).map(Self) } + /// Returns the little-endian bit representation of the scalar element. + fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Parses a scalar from little-endian bits. + #[staticmethod] + fn from_bits_le(bits: Vec<bool>) -> anyhow::Result<Self> { + ScalarNative::from_bits_le(&bits).map(Self) + } + /// Returns this scalar as a field element. fn to_field(&self) -> anyhow::Result<Field> { self.0.to_field().map(Into::into) } + /// Doubles this scalar element. + fn double(&self) -> Self { + Self(Double::double(&self.0)) + } + /// Returns the sum of self and other. fn add(&self, other: Self) -> Self { Self(self.0 + other.0) @@ -84,7 +116,7 @@ impl Scalar { Self(self.0 * other.0) } - /// Returns self divided by other (panics on zero divisor). + /// Returns self divided by other (errors on zero divisor). fn divide(&self, other: Self) -> PyResult<Self> { if other.is_zero() { Err(PyZeroDivisionError::new_err("division by zero")) @@ -103,6 +135,125 @@ impl Scalar { Self(-self.0) } + /// Converts to a Plaintext wrapper. + fn to_plaintext(&self) -> crate::Plaintext { + crate::Plaintext::from(PlaintextNative::Literal( + LiteralNative::Scalar(self.0), + OnceLock::new(), + )) + } + + // ── cast conversions ─────────────────────────────────────────────────── + + /// Converts to a Boolean (strict: must be zero or one). + fn to_boolean(&self) -> anyhow::Result<Boolean> { + if self.0.is_zero() { + Ok(Boolean::from(crate::types::BooleanNative::new(false))) + } else if self.0.is_one() { + Ok(Boolean::from(crate::types::BooleanNative::new(true))) + } else { + Err(anyhow::anyhow!( + "Failed to convert scalar to boolean: scalar is not zero or one" + )) + } + } + + /// Converts to a Boolean with lossy truncation (LSB). + fn to_boolean_lossy(&self) -> Boolean { + Boolean::from(crate::types::BooleanNative::new(self.0.to_bits_le()[0])) + } + + /// Converts to a Group element (strict, via Field x-coordinate recovery). + fn to_group(&self) -> anyhow::Result<Group> { + let f: FieldNative = self.0.to_field()?; + GroupNative::from_field(&f).map(Into::into) + } + + /// Converts to a Group element with lossy conversion (Elligator-2 fallback). + fn to_group_lossy(&self) -> anyhow::Result<Group> { + let f: FieldNative = self.0.to_field()?; + let g: GroupNative = f.cast_lossy(); + Ok(Group::from(g)) + } + + /// Converts to an Address (strict, via Group x-coordinate recovery). + fn to_address(&self) -> anyhow::Result<Address> { + let f: FieldNative = self.0.to_field()?; + let g = GroupNative::from_field(&f)?; + Ok(Address::new(g)) + } + + /// Converts to an Address with lossy conversion. + fn to_address_lossy(&self) -> anyhow::Result<Address> { + let f: FieldNative = self.0.to_field()?; + let g: GroupNative = f.cast_lossy(); + Ok(Address::new(g)) + } + + // Scalar → Integer lossy conversions (via field) + + /// Cast to U8 with lossy truncation. + fn to_u8_lossy(&self) -> anyhow::Result<crate::U8> { + let f = self.0.to_field()?; + Ok(crate::U8::from(U8Native::from_field_lossy(&f))) + } + + /// Cast to U16 with lossy truncation. + fn to_u16_lossy(&self) -> anyhow::Result<crate::U16> { + let f = self.0.to_field()?; + Ok(crate::U16::from(U16Native::from_field_lossy(&f))) + } + + /// Cast to U32 with lossy truncation. + fn to_u32_lossy(&self) -> anyhow::Result<crate::U32> { + let f = self.0.to_field()?; + Ok(crate::U32::from(U32Native::from_field_lossy(&f))) + } + + /// Cast to U64 with lossy truncation. + fn to_u64_lossy(&self) -> anyhow::Result<crate::U64> { + let f = self.0.to_field()?; + Ok(crate::U64::from(U64Native::from_field_lossy(&f))) + } + + /// Cast to U128 with lossy truncation. + fn to_u128_lossy(&self) -> anyhow::Result<crate::U128> { + let f = self.0.to_field()?; + Ok(crate::U128::from(U128Native::from_field_lossy(&f))) + } + + /// Cast to I8 with lossy truncation. + fn to_i8_lossy(&self) -> anyhow::Result<crate::I8> { + let f = self.0.to_field()?; + Ok(crate::I8::from(I8Native::from_field_lossy(&f))) + } + + /// Cast to I16 with lossy truncation. + fn to_i16_lossy(&self) -> anyhow::Result<crate::I16> { + let f = self.0.to_field()?; + Ok(crate::I16::from(I16Native::from_field_lossy(&f))) + } + + /// Cast to I32 with lossy truncation. + fn to_i32_lossy(&self) -> anyhow::Result<crate::I32> { + let f = self.0.to_field()?; + Ok(crate::I32::from(I32Native::from_field_lossy(&f))) + } + + /// Cast to I64 with lossy truncation. + fn to_i64_lossy(&self) -> anyhow::Result<crate::I64> { + let f = self.0.to_field()?; + Ok(crate::I64::from(I64Native::from_field_lossy(&f))) + } + + /// Cast to I128 with lossy truncation. + fn to_i128_lossy(&self) -> anyhow::Result<crate::I128> { + let f = self.0.to_field()?; + Ok(crate::I128::from(I128Native::from_field_lossy(&f))) + } + + // ── dunders ──────────────────────────────────────────────────────────── + /// Returns the Scalar as a string. fn __str__(&self) -> String { self.0.to_string() From ed0e64b0a6023d473824e819bbc5c2ad368462d9 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 12:14:38 -0400 Subject: [PATCH 41/72] =?UTF-8?q?fix:=20use=20stdlib=20checked=20ops=20for?= =?UTF-8?q?=20integer=20arithmetic=20=E2=80=94=20no=20panics,=20clean=20ex?= =?UTF-8?q?ceptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace catch_unwind/wrap_overflow with primitive checked_add/sub/mul/div/pow and guarded rem/rem_wrapped; division and remainder by zero now raise ZeroDivisionError, other failures raise OverflowError without any panic. Fix vacuous Field.random test and add rem zero-divisor + I8 MIN%-1 pinned tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_numeric_parity.py | 33 ++- sdk/src/algebra/integer.rs | 257 +++++++++++++++++------- 2 files changed, 212 insertions(+), 78 deletions(-) diff --git a/sdk/python/tests/test_numeric_parity.py b/sdk/python/tests/test_numeric_parity.py index 29d2593..d67460c 100644 --- a/sdk/python/tests/test_numeric_parity.py +++ b/sdk/python/tests/test_numeric_parity.py @@ -63,7 +63,7 @@ def test_dunder_mul_eq_multiply(self): assert a * b == a.multiply(b) def test_div_zero_raises(self): - with pytest.raises(OverflowError): + with pytest.raises(ZeroDivisionError): U8(5) // U8(0) def test_floordiv_eq_divide(self): @@ -85,9 +85,17 @@ def test_div_wrapped(self): def test_rem(self): assert U8(10).rem(U8(3)) == U8(1) + def test_rem_zero_divisor_raises(self): + with pytest.raises(ZeroDivisionError): + U8(10).rem(U8(0)) + def test_rem_wrapped(self): assert U8(10).rem_wrapped(U8(3)) == U8(1) + def test_rem_wrapped_zero_divisor_raises(self): + with pytest.raises(ZeroDivisionError): + U8(10).rem_wrapped(U8(0)) + def test_pow_u8(self): # 2^8 = 256 overflows u8 with pytest.raises(OverflowError): @@ -232,6 +240,23 @@ def test_to_field_from_field(self): v = I8(-42) assert I8.from_field(v.to_field()) == v + def test_rem_zero_divisor_raises(self): + with pytest.raises(ZeroDivisionError): + I8(10).rem(I8(0)) + + def test_rem_wrapped_zero_divisor_raises(self): + with pytest.raises(ZeroDivisionError): + I8(10).rem_wrapped(I8(0)) + + def test_rem_min_neg1_raises_overflow(self): + # i8::MIN % -1 overflows (checked_rem returns None); should raise OverflowError + with pytest.raises(OverflowError): + I8(-128).rem(I8(-1)) + + def test_rem_wrapped_min_neg1(self): + # rem_wrapped uses wrapping_rem: i8::MIN wrapping_rem -1 == 0 in Rust stdlib + assert I8(-128).rem_wrapped(I8(-1)) == I8(0) + # ═══════════════════════════════════════════════════════════════ # Integer — I128 (signed, large) @@ -467,8 +492,10 @@ def test_to_i8_lossy(self): def test_random_distinct(self): a, b = Field.random(), Field.random() - # Astronomically unlikely to collide - assert a != b or True # just ensure it runs + assert isinstance(a, Field) + assert isinstance(b, Field) + # Astronomically unlikely to collide over BLS12-377 field + assert a != b # ═══════════════════════════════════════════════════════════════ diff --git a/sdk/src/algebra/integer.rs b/sdk/src/algebra/integer.rs index 02bf60f..82ca8de 100644 --- a/sdk/src/algebra/integer.rs +++ b/sdk/src/algebra/integer.rs @@ -22,10 +22,13 @@ use crate::{ Boolean, Field, Scalar, }; -use pyo3::{exceptions::PyOverflowError, prelude::*}; +use pyo3::{ + exceptions::{PyOverflowError, PyZeroDivisionError}, + prelude::*, +}; use rand::rngs::StdRng; use snarkvm::prelude::{ - AddWrapped, DivWrapped, FromBits, FromBytes, FromField, FromFields, MulWrapped, One, Pow, Rem, + AddWrapped, DivWrapped, FromBits, FromBytes, FromField, FromFields, MulWrapped, One, RemWrapped, SubWrapped, ToBits, ToBytes, ToField, Uniform, Zero, }; @@ -36,20 +39,6 @@ use std::{ sync::OnceLock, }; -// ── helper: convert a snarkvm panic ("halt") into a Python OverflowError ────── -fn wrap_overflow<T, F: FnOnce() -> T + std::panic::UnwindSafe>(f: F) -> PyResult<T> { - std::panic::catch_unwind(f).map_err(|e| { - let msg = if let Some(s) = e.downcast_ref::<String>() { - s.clone() - } else if let Some(s) = e.downcast_ref::<&str>() { - s.to_string() - } else { - "integer arithmetic overflow or panic".to_string() - }; - PyOverflowError::new_err(msg) - }) -} - // ── Unsigned integer macro ──────────────────────────────────────────────────── // All methods for unsigned integers in a single #[pymethods] block. @@ -155,27 +144,54 @@ macro_rules! integer_unsigned { // ── checked arithmetic ───────────────────────────────────────── fn add(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a + b)) + (*self.0) + .checked_add(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " addition overflowed" + )) + }) } fn subtract(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a - b)) + (*self.0) + .checked_sub(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " subtraction overflowed" + )) + }) } fn multiply(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a * b)) + (*self.0) + .checked_mul(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " multiplication overflowed" + )) + }) } fn divide(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a / b)) + if *other.0 == 0 { + return Err(PyZeroDivisionError::new_err("integer division by zero")); + } + (*self.0) + .checked_div(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " division overflowed" + )) + }) } // ── wrapped ──────────────────────────────────────────────────── @@ -198,32 +214,59 @@ macro_rules! integer_unsigned { // ── remainder ───────────────────────────────────────────────── - fn rem(&self, other: Self) -> Self { - Self(self.0.rem(&other.0)) - } - - fn rem_wrapped(&self, other: Self) -> Self { - Self(self.0.rem_wrapped(&other.0)) + fn rem(&self, other: Self) -> PyResult<Self> { + if *other.0 == 0 { + return Err(PyZeroDivisionError::new_err("integer remainder by zero")); + } + // For unsigned types, checked_rem only fails on zero divisor (already handled). + (*self.0) + .checked_rem(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " remainder overflowed" + )) + }) + } + + fn rem_wrapped(&self, other: Self) -> PyResult<Self> { + if *other.0 == 0 { + return Err(PyZeroDivisionError::new_err("integer remainder by zero")); + } + Ok(Self(self.0.rem_wrapped(&other.0))) } // ── power ────────────────────────────────────────────────────── fn pow_u8(&self, exp: U8) -> PyResult<Self> { - let base = self.0; - let e = exp.0; - wrap_overflow(move || Self(base.pow(&e))) + let exp_u32 = *exp.0 as u32; + (*self.0) + .checked_pow(exp_u32) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!(stringify!($machine), " power overflowed")) + }) } fn pow_u16(&self, exp: U16) -> PyResult<Self> { - let base = self.0; - let e = exp.0; - wrap_overflow(move || Self(base.pow(&e))) + let exp_u32 = *exp.0 as u32; + (*self.0) + .checked_pow(exp_u32) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!(stringify!($machine), " power overflowed")) + }) } fn pow_u32(&self, exp: U32) -> PyResult<Self> { - let base = self.0; - let e = exp.0; - wrap_overflow(move || Self(base.pow(&e))) + let exp_u32 = *exp.0; + (*self.0) + .checked_pow(exp_u32) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!(stringify!($machine), " power overflowed")) + }) } // ── conversions ──────────────────────────────────────────────── @@ -461,38 +504,75 @@ macro_rules! integer_signed { // ── checked arithmetic ───────────────────────────────────────── fn add(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a + b)) + (*self.0) + .checked_add(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " addition overflowed" + )) + }) } fn subtract(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a - b)) + (*self.0) + .checked_sub(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " subtraction overflowed" + )) + }) } fn multiply(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a * b)) + (*self.0) + .checked_mul(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " multiplication overflowed" + )) + }) } fn divide(&self, other: Self) -> PyResult<Self> { - let a = self.0; - let b = other.0; - wrap_overflow(move || Self(a / b)) + if *other.0 == 0 { + return Err(PyZeroDivisionError::new_err("integer division by zero")); + } + (*self.0) + .checked_div(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " division overflowed" + )) + }) } fn negate(&self) -> PyResult<Self> { - let v = self.0; - wrap_overflow(move || Self(-v)) + (*self.0) + .checked_neg() + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " negation overflowed" + )) + }) } fn abs_checked(&self) -> PyResult<Self> { - use snarkvm::prelude::AbsChecked; - let v = self.0; - wrap_overflow(move || Self(v.abs_checked())) + (*self.0) + .checked_abs() + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!(stringify!($machine), " abs overflowed")) + }) } fn abs_wrapped(&self) -> Self { @@ -520,32 +600,59 @@ macro_rules! integer_signed { // ── remainder ───────────────────────────────────────────────── - fn rem(&self, other: Self) -> Self { - Self(self.0.rem(&other.0)) - } - - fn rem_wrapped(&self, other: Self) -> Self { - Self(self.0.rem_wrapped(&other.0)) + fn rem(&self, other: Self) -> PyResult<Self> { + if *other.0 == 0 { + return Err(PyZeroDivisionError::new_err("integer remainder by zero")); + } + // For signed types, checked_rem also returns None for MIN % -1 (overflow). + (*self.0) + .checked_rem(*other.0) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!( + stringify!($machine), + " remainder overflowed" + )) + }) + } + + fn rem_wrapped(&self, other: Self) -> PyResult<Self> { + if *other.0 == 0 { + return Err(PyZeroDivisionError::new_err("integer remainder by zero")); + } + Ok(Self(self.0.rem_wrapped(&other.0))) } // ── power ────────────────────────────────────────────────────── fn pow_u8(&self, exp: U8) -> PyResult<Self> { - let base = self.0; - let e = exp.0; - wrap_overflow(move || Self(base.pow(&e))) + let exp_u32 = *exp.0 as u32; + (*self.0) + .checked_pow(exp_u32) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!(stringify!($machine), " power overflowed")) + }) } fn pow_u16(&self, exp: U16) -> PyResult<Self> { - let base = self.0; - let e = exp.0; - wrap_overflow(move || Self(base.pow(&e))) + let exp_u32 = *exp.0 as u32; + (*self.0) + .checked_pow(exp_u32) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!(stringify!($machine), " power overflowed")) + }) } fn pow_u32(&self, exp: U32) -> PyResult<Self> { - let base = self.0; - let e = exp.0; - wrap_overflow(move || Self(base.pow(&e))) + let exp_u32 = *exp.0; + (*self.0) + .checked_pow(exp_u32) + .map(|v| Self($native::new(v))) + .ok_or_else(|| { + PyOverflowError::new_err(concat!(stringify!($machine), " power overflowed")) + }) } // ── conversions ──────────────────────────────────────────────── From debe60140caed3d5ff9f28d6e2bb8a1d4b2cc74b Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 12:38:41 -0400 Subject: [PATCH 42/72] feat: add ProofNative, DynamicRecordNative, StatePathNative type aliases --- sdk/src/types.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sdk/src/types.rs b/sdk/src/types.rs index f728b88..f05afb7 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -101,3 +101,12 @@ pub type InputNative = Input<CurrentNetwork>; pub type OutputNative = Output<CurrentNetwork>; pub type FutureNative = Future<CurrentNetwork>; pub type ArgumentNative = Argument<CurrentNetwork>; + +// Proof type +pub type ProofNative = snarkvm::synthesizer::snark::Proof<CurrentNetwork>; + +// Dynamic record type +pub type DynamicRecordNative = snarkvm::console::program::DynamicRecord<CurrentNetwork>; + +// State path type (for OfflineQuery) +pub type StatePathNative = snarkvm::console::program::StatePath<CurrentNetwork>; From 36b7add2538314bded2db9a41d3f2c6b013c27c8 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 12:39:38 -0400 Subject: [PATCH 43/72] feat: add Proof type with from_string/from_bytes/bytes/__str__/__eq__ --- sdk/python/tests/test_program_keys.py | 13 +++++ sdk/src/lib.rs | 1 + sdk/src/programs/mod.rs | 3 ++ sdk/src/programs/proof.rs | 68 +++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 sdk/python/tests/test_program_keys.py create mode 100644 sdk/src/programs/proof.rs diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py new file mode 100644 index 0000000..3f6ac7e --- /dev/null +++ b/sdk/python/tests/test_program_keys.py @@ -0,0 +1,13 @@ +"""Tests for W4d2 wasm-parity additions.""" +import pytest +from aleo.mainnet import Proof + + +def test_proof_from_invalid_string(): + with pytest.raises(Exception): + Proof.from_string("not_a_proof") + + +def test_proof_from_invalid_bytes(): + with pytest.raises(Exception): + Proof.from_bytes(bytes(10)) diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index e4be2b8..526e790 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -74,6 +74,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Poseidon8>()?; m.add_class::<PrivateKey>()?; m.add_class::<Process>()?; + m.add_class::<Proof>()?; m.add_class::<Program>()?; m.add_class::<ProgramID>()?; m.add_class::<ProvingKey>()?; diff --git a/sdk/src/programs/mod.rs b/sdk/src/programs/mod.rs index e180c28..cbac558 100644 --- a/sdk/src/programs/mod.rs +++ b/sdk/src/programs/mod.rs @@ -62,5 +62,8 @@ pub use transition::Transition; mod value; pub use value::Value; +mod proof; +pub use proof::Proof; + mod verifying_key; pub use verifying_key::VerifyingKey; diff --git a/sdk/src/programs/proof.rs b/sdk/src/programs/proof.rs new file mode 100644 index 0000000..4c6b15f --- /dev/null +++ b/sdk/src/programs/proof.rs @@ -0,0 +1,68 @@ +// Copyright (C) 2019-2023 Aleo Systems Inc. +// This file is part of the Aleo SDK library. +// +// Licensed under GPL-3.0-or-later. + +use crate::types::ProofNative; + +use pyo3::prelude::*; +use snarkvm::prelude::{FromBytes, ToBytes}; + +use std::{ops::Deref, str::FromStr}; + +/// SNARK proof for verification of program execution. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct Proof(ProofNative); + +#[pymethods] +impl Proof { + /// Construct a new proof from a byte array. + #[staticmethod] + fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> { + ProofNative::from_bytes_le(bytes).map(Self) + } + + /// Create a proof from its string representation. + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + ProofNative::from_str(s).map(Self) + } + + /// Return the byte representation of the proof. + fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Returns the proof as a string. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } + + #[classattr] + const __hash__: Option<PyObject> = None; +} + +impl Deref for Proof { + type Target = ProofNative; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<ProofNative> for Proof { + fn from(proof: ProofNative) -> Self { + Self(proof) + } +} + +impl From<Proof> for ProofNative { + fn from(proof: Proof) -> Self { + proof.0 + } +} From 5d21b3b9382520bcb2a99b1659d70f5fe9d95c2e Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 12:42:03 -0400 Subject: [PATCH 44/72] feat: add DynamicRecord type with from_record/to_record/bytes/fields/bits --- sdk/python/tests/test_program_keys.py | 65 ++++++++++++++- sdk/src/lib.rs | 1 + sdk/src/programs/dynamic_record.rs | 116 ++++++++++++++++++++++++++ sdk/src/programs/mod.rs | 3 + 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 sdk/src/programs/dynamic_record.rs diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index 3f6ac7e..5f976f0 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -1,6 +1,6 @@ """Tests for W4d2 wasm-parity additions.""" import pytest -from aleo.mainnet import Proof +from aleo.mainnet import Proof, DynamicRecord, RecordPlaintext def test_proof_from_invalid_string(): @@ -11,3 +11,66 @@ def test_proof_from_invalid_string(): def test_proof_from_invalid_bytes(): with pytest.raises(Exception): Proof.from_bytes(bytes(10)) + + +CREDITS_RECORD_V1 = ( + "{ owner: aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a.private, " + "microcredits: 1000000u64.private, " + "_nonce: 3634848344765318974603121890869676775499130077229666060613233255327643175219group.public, " + "_version: 1u8.public }" +) +EXPECTED_OWNER = "aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a" +EXPECTED_ROOT = "3632128850012040781624982531669233558118256132998845003264257146683715587370field" +EXPECTED_NONCE = "3634848344765318974603121890869676775499130077229666060613233255327643175219group" + + +def test_dynamic_record_from_record_round_trip(): + """KAT: DynamicRecord from RecordPlaintext and back.""" + record = RecordPlaintext.from_string(CREDITS_RECORD_V1) + dr = DynamicRecord.from_record(record) + assert str(dr.owner) == EXPECTED_OWNER + assert str(dr.root) == EXPECTED_ROOT + assert str(dr.nonce) == EXPECTED_NONCE + assert dr.is_hiding() is True + + +def test_dynamic_record_string_round_trip(): + record = RecordPlaintext.from_string(CREDITS_RECORD_V1) + dr = DynamicRecord.from_record(record) + dr_str = str(dr) + dr2 = DynamicRecord.from_string(dr_str) + assert str(dr2) == dr_str + + +def test_dynamic_record_bytes_round_trip(): + record = RecordPlaintext.from_string(CREDITS_RECORD_V1) + dr = DynamicRecord.from_record(record) + raw = dr.bytes() + dr2 = DynamicRecord.from_bytes(bytes(raw)) + assert str(dr2.owner) == EXPECTED_OWNER + + +def test_dynamic_record_to_record(): + record = RecordPlaintext.from_string(CREDITS_RECORD_V1) + dr = DynamicRecord.from_record(record) + back = dr.to_record(owner_is_private=True) + assert back is not None + + +def test_dynamic_record_to_fields(): + record = RecordPlaintext.from_string(CREDITS_RECORD_V1) + dr = DynamicRecord.from_record(record) + fields = dr.to_fields() + assert len(fields) > 0 + + +def test_dynamic_record_to_bits_le(): + record = RecordPlaintext.from_string(CREDITS_RECORD_V1) + dr = DynamicRecord.from_record(record) + bits = dr.to_bits_le() + assert len(bits) > 0 + + +def test_dynamic_record_invalid(): + with pytest.raises(Exception): + DynamicRecord.from_string("not a record") diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 526e790..dee334f 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -50,6 +50,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Ciphertext>()?; m.add_class::<ComputeKey>()?; m.add_class::<Credits>()?; + m.add_class::<DynamicRecord>()?; m.add_class::<Execution>()?; m.add_class::<Fee>()?; m.add_class::<Field>()?; diff --git a/sdk/src/programs/dynamic_record.rs b/sdk/src/programs/dynamic_record.rs new file mode 100644 index 0000000..2e6005b --- /dev/null +++ b/sdk/src/programs/dynamic_record.rs @@ -0,0 +1,116 @@ +// Copyright (C) 2019-2023 Aleo Systems Inc. +// This file is part of the Aleo SDK library. +// +// Licensed under GPL-3.0-or-later. + +use crate::{ + types::DynamicRecordNative, + Address, Field, Group, RecordPlaintext, +}; + +use pyo3::prelude::*; +use snarkvm::prelude::{FromBytes, ToBits, ToBytes, ToFields}; + +use std::{ops::Deref, str::FromStr}; + +/// A fixed-size representation of an Aleo record that stores the Merkle root +/// of the record data rather than the full data. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct DynamicRecord(DynamicRecordNative); + +#[pymethods] +impl DynamicRecord { + /// Creates a DynamicRecord from its string representation. + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + DynamicRecordNative::from_str(s).map(Self) + } + + /// Creates a DynamicRecord from a RecordPlaintext. + #[staticmethod] + fn from_record(record: &RecordPlaintext) -> anyhow::Result<Self> { + DynamicRecordNative::from_record(&**record).map(Self) + } + + /// Deserializes a DynamicRecord from a little-endian byte array. + #[staticmethod] + fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> { + DynamicRecordNative::from_bytes_le(bytes).map(Self) + } + + /// Converts this DynamicRecord back to a RecordPlaintext. + fn to_record(&self, owner_is_private: bool) -> anyhow::Result<RecordPlaintext> { + self.0.to_record(owner_is_private).map(RecordPlaintext::from) + } + + /// Serializes the dynamic record to a little-endian byte array. + fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Returns the dynamic record as a vector of field elements. + fn to_fields(&self) -> anyhow::Result<Vec<Field>> { + self.0.to_fields().map(|fs| fs.into_iter().map(Field::from).collect()) + } + + /// Returns the dynamic record as a little-endian bit vector. + fn to_bits_le(&self) -> Vec<bool> { + self.0.to_bits_le() + } + + /// Returns the owner address of the dynamic record. + #[getter] + fn owner(&self) -> Address { + Address::from(*self.0.owner()) + } + + /// Returns the Merkle root of the record data as a Field. + #[getter] + fn root(&self) -> Field { + Field::from(*self.0.root()) + } + + /// Returns the nonce of the record as a Group. + #[getter] + fn nonce(&self) -> Group { + Group::from(*self.0.nonce()) + } + + /// Returns true if the dynamic record is a hiding variant (version != 0). + fn is_hiding(&self) -> bool { + self.0.is_hiding() + } + + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + // Compare via string representation + self.0.to_string() == other.0.to_string() + } + + #[classattr] + const __hash__: Option<PyObject> = None; +} + +impl Deref for DynamicRecord { + type Target = DynamicRecordNative; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<DynamicRecordNative> for DynamicRecord { + fn from(native: DynamicRecordNative) -> Self { + Self(native) + } +} + +impl From<DynamicRecord> for DynamicRecordNative { + fn from(dr: DynamicRecord) -> Self { + dr.0 + } +} diff --git a/sdk/src/programs/mod.rs b/sdk/src/programs/mod.rs index cbac558..2160df9 100644 --- a/sdk/src/programs/mod.rs +++ b/sdk/src/programs/mod.rs @@ -17,6 +17,9 @@ mod authorization; pub use authorization::Authorization; +mod dynamic_record; +pub use dynamic_record::DynamicRecord; + mod execution; pub use execution::Execution; From 8be0d9fbe9795fbf89e80b03712d86d94ffd428e Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 12:44:02 -0400 Subject: [PATCH 45/72] feat: add Metadata type with 15 credits statics and base_url --- sdk/python/tests/test_program_keys.py | 28 +- sdk/src/lib.rs | 1 + sdk/src/programs/metadata.rs | 395 ++++++++++++++++++++++++++ sdk/src/programs/mod.rs | 3 + 4 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 sdk/src/programs/metadata.rs diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index 5f976f0..c777cbf 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -1,6 +1,6 @@ """Tests for W4d2 wasm-parity additions.""" import pytest -from aleo.mainnet import Proof, DynamicRecord, RecordPlaintext +from aleo.mainnet import Proof, DynamicRecord, RecordPlaintext, Metadata def test_proof_from_invalid_string(): @@ -74,3 +74,29 @@ def test_dynamic_record_to_bits_le(): def test_dynamic_record_invalid(): with pytest.raises(Exception): DynamicRecord.from_string("not a record") + + +def test_metadata_bond_public(): + m = Metadata.bond_public() + assert m.name == "bond_public" + assert m.locator == "credits.aleo/bond_public" + assert m.verifying_key == "bondPublicVerifier" + assert "bond_public.prover." in m.prover + assert "bond_public.verifier." in m.verifier + assert "parameters.provable.com/mainnet" in m.prover + + +def test_metadata_transfer_public(): + m = Metadata.transfer_public() + assert m.name == "transfer_public" + assert m.locator == "credits.aleo/transfer_public" + + +def test_metadata_inclusion(): + m = Metadata.inclusion() + assert m.name == "inclusion" + assert m.locator == "inclusion" + + +def test_metadata_base_url(): + assert "parameters.provable.com" in Metadata.base_url() diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index dee334f..59b0cf4 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -65,6 +65,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Literal>()?; m.add_class::<Locator>()?; m.add_class::<MicroCredits>()?; + m.add_class::<Metadata>()?; m.add_class::<Network>()?; m.add_class::<Pedersen64>()?; m.add_class::<Pedersen128>()?; diff --git a/sdk/src/programs/metadata.rs b/sdk/src/programs/metadata.rs new file mode 100644 index 0000000..60c77a6 --- /dev/null +++ b/sdk/src/programs/metadata.rs @@ -0,0 +1,395 @@ +// Copyright (C) 2019-2023 Aleo Systems Inc. +// This file is part of the Aleo SDK library. +// +// Licensed under GPL-3.0-or-later. + +use pyo3::prelude::*; + +#[cfg(not(feature = "testnet"))] +const BASE_URL: &str = "https://parameters.provable.com/mainnet/"; +#[cfg(feature = "testnet")] +const BASE_URL: &str = "https://parameters.provable.com/testnet/"; + +fn prover_checksum(metadata_json: &'static str) -> String { + let meta: serde_json::Value = + serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); + meta["prover_checksum"].as_str().expect("Failed to parse prover checksum").to_string() +} + +fn verifier_checksum(metadata_json: &'static str) -> String { + let meta: serde_json::Value = + serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); + meta["verifier_checksum"].as_str().expect("Failed to parse verifier checksum").to_string() +} + +fn make_metadata( + name: &str, + verifying_key_js_name: &str, + locator: &str, + metadata_json: &'static str, +) -> Metadata { + let pc = prover_checksum(metadata_json); + let vc = verifier_checksum(metadata_json); + Metadata { + name: name.to_string(), + locator: locator.to_string(), + prover: format!("{}{}.prover.{}", BASE_URL, name, &pc[..7]), + verifier: format!("{}.verifier.{}", name, &vc[..7]), + verifying_key: verifying_key_js_name.to_string(), + } +} + +/// Metadata for an Aleo credits function's proving and verifying keys. +#[pyclass] +#[derive(Clone)] +pub struct Metadata { + #[pyo3(get)] + pub name: String, + #[pyo3(get)] + pub locator: String, + #[pyo3(get)] + pub prover: String, + #[pyo3(get)] + pub verifier: String, + #[pyo3(get)] + pub verifying_key: String, +} + +#[pymethods] +impl Metadata { + /// Returns the base URL for parameter downloads. + #[staticmethod] + fn base_url() -> String { + BASE_URL.to_string() + } + + #[staticmethod] + fn bond_public() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::BondPublicProver; + make_metadata( + "bond_public", + "bondPublicVerifier", + "credits.aleo/bond_public", + BondPublicProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::BondPublicProver; + make_metadata( + "bond_public", + "bondPublicVerifier", + "credits.aleo/bond_public", + BondPublicProver::METADATA, + ) + } + } + + #[staticmethod] + fn bond_validator() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::BondValidatorProver; + make_metadata( + "bond_validator", + "bondValidatorVerifier", + "credits.aleo/bond_validator", + BondValidatorProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::BondValidatorProver; + make_metadata( + "bond_validator", + "bondValidatorVerifier", + "credits.aleo/bond_validator", + BondValidatorProver::METADATA, + ) + } + } + + #[staticmethod] + fn claim_unbond_public() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::ClaimUnbondPublicProver; + make_metadata( + "claim_unbond_public", + "claimUnbondPublicVerifier", + "credits.aleo/claim_unbond_public", + ClaimUnbondPublicProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::ClaimUnbondPublicProver; + make_metadata( + "claim_unbond_public", + "claimUnbondPublicVerifier", + "credits.aleo/claim_unbond_public", + ClaimUnbondPublicProver::METADATA, + ) + } + } + + #[staticmethod] + fn fee_private() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::FeePrivateProver; + make_metadata( + "fee_private", + "feePrivateVerifier", + "credits.aleo/fee_private", + FeePrivateProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::FeePrivateProver; + make_metadata( + "fee_private", + "feePrivateVerifier", + "credits.aleo/fee_private", + FeePrivateProver::METADATA, + ) + } + } + + #[staticmethod] + fn fee_public() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::FeePublicProver; + make_metadata( + "fee_public", + "feePublicVerifier", + "credits.aleo/fee_public", + FeePublicProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::FeePublicProver; + make_metadata( + "fee_public", + "feePublicVerifier", + "credits.aleo/fee_public", + FeePublicProver::METADATA, + ) + } + } + + #[staticmethod] + fn inclusion() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::InclusionProver; + make_metadata("inclusion", "inclusionVerifier", "inclusion", InclusionProver::METADATA) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::InclusionProver; + make_metadata("inclusion", "inclusionVerifier", "inclusion", InclusionProver::METADATA) + } + } + + #[staticmethod] + fn join() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::JoinProver; + make_metadata("join", "joinVerifier", "credits.aleo/join", JoinProver::METADATA) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::JoinProver; + make_metadata("join", "joinVerifier", "credits.aleo/join", JoinProver::METADATA) + } + } + + #[staticmethod] + fn set_validator_state() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::SetValidatorStateProver; + make_metadata( + "set_validator_state", + "setValidatorStateVerifier", + "credits.aleo/set_validator_state", + SetValidatorStateProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::SetValidatorStateProver; + make_metadata( + "set_validator_state", + "setValidatorStateVerifier", + "credits.aleo/set_validator_state", + SetValidatorStateProver::METADATA, + ) + } + } + + #[staticmethod] + fn split() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::SplitProver; + make_metadata("split", "splitVerifier", "credits.aleo/split", SplitProver::METADATA) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::SplitProver; + make_metadata("split", "splitVerifier", "credits.aleo/split", SplitProver::METADATA) + } + } + + #[staticmethod] + fn transfer_private() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::TransferPrivateProver; + make_metadata( + "transfer_private", + "transferPrivateVerifier", + "credits.aleo/transfer_private", + TransferPrivateProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::TransferPrivateProver; + make_metadata( + "transfer_private", + "transferPrivateVerifier", + "credits.aleo/transfer_private", + TransferPrivateProver::METADATA, + ) + } + } + + #[staticmethod] + fn transfer_private_to_public() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::TransferPrivateToPublicProver; + make_metadata( + "transfer_private_to_public", + "transferPrivateToPublicVerifier", + "credits.aleo/transfer_private_to_public", + TransferPrivateToPublicProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::TransferPrivateToPublicProver; + make_metadata( + "transfer_private_to_public", + "transferPrivateToPublicVerifier", + "credits.aleo/transfer_private_to_public", + TransferPrivateToPublicProver::METADATA, + ) + } + } + + #[staticmethod] + fn transfer_public() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::TransferPublicProver; + make_metadata( + "transfer_public", + "transferPublicVerifier", + "credits.aleo/transfer_public", + TransferPublicProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::TransferPublicProver; + make_metadata( + "transfer_public", + "transferPublicVerifier", + "credits.aleo/transfer_public", + TransferPublicProver::METADATA, + ) + } + } + + #[staticmethod] + fn transfer_public_as_signer() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::TransferPublicAsSignerProver; + make_metadata( + "transfer_public_as_signer", + "transferPublicAsSignerVerifier", + "credits.aleo/transfer_public_as_signer", + TransferPublicAsSignerProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::TransferPublicAsSignerProver; + make_metadata( + "transfer_public_as_signer", + "transferPublicAsSignerVerifier", + "credits.aleo/transfer_public_as_signer", + TransferPublicAsSignerProver::METADATA, + ) + } + } + + #[staticmethod] + fn transfer_public_to_private() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::TransferPublicToPrivateProver; + make_metadata( + "transfer_public_to_private", + "transferPublicToPrivateVerifier", + "credits.aleo/transfer_public_to_private", + TransferPublicToPrivateProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::TransferPublicToPrivateProver; + make_metadata( + "transfer_public_to_private", + "transferPublicToPrivateVerifier", + "credits.aleo/transfer_public_to_private", + TransferPublicToPrivateProver::METADATA, + ) + } + } + + #[staticmethod] + fn unbond_public() -> Metadata { + #[cfg(not(feature = "testnet"))] + { + use snarkvm::parameters::mainnet::UnbondPublicProver; + make_metadata( + "unbond_public", + "unbondPublicVerifier", + "credits.aleo/unbond_public", + UnbondPublicProver::METADATA, + ) + } + #[cfg(feature = "testnet")] + { + use snarkvm::parameters::testnet::UnbondPublicProver; + make_metadata( + "unbond_public", + "unbondPublicVerifier", + "credits.aleo/unbond_public", + UnbondPublicProver::METADATA, + ) + } + } +} diff --git a/sdk/src/programs/mod.rs b/sdk/src/programs/mod.rs index 2160df9..c04838c 100644 --- a/sdk/src/programs/mod.rs +++ b/sdk/src/programs/mod.rs @@ -35,6 +35,9 @@ pub use literal::Literal; mod locator; pub use locator::Locator; +mod metadata; +pub use metadata::Metadata; + mod process; pub use process::Process; From 68a0c23c06cb09d0491b2a7840e8424efd89fde9 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 12:46:30 -0400 Subject: [PATCH 46/72] feat: add OfflineQuery type with KAT round-trip --- sdk/python/tests/test_program_keys.py | 23 +++++++++- sdk/src/lib.rs | 1 + sdk/src/programs/dynamic_record.rs | 13 +++--- sdk/src/programs/metadata.rs | 52 ++++++++++++++++++---- sdk/src/programs/mod.rs | 3 ++ sdk/src/programs/offline_query.rs | 63 +++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 sdk/src/programs/offline_query.rs diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index c777cbf..bb4ba82 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -1,6 +1,6 @@ """Tests for W4d2 wasm-parity additions.""" import pytest -from aleo.mainnet import Proof, DynamicRecord, RecordPlaintext, Metadata +from aleo.mainnet import Proof, DynamicRecord, RecordPlaintext, Metadata, OfflineQuery def test_proof_from_invalid_string(): @@ -100,3 +100,24 @@ def test_metadata_inclusion(): def test_metadata_base_url(): assert "parameters.provable.com" in Metadata.base_url() + + +OFFLINE_QUERY_KAT = '{"block_height":0,"state_paths":{},"state_root":"sr1wjueje6hy86yw9j4lhl7jwvhjxwunw34paj4k3cn2wm5h5r2syfqd83yw4"}' +STATE_ROOT = "sr1wjueje6hy86yw9j4lhl7jwvhjxwunw34paj4k3cn2wm5h5r2syfqd83yw4" + + +def test_offline_query_kat(): + oq = OfflineQuery.new(0, STATE_ROOT) + assert str(oq) == OFFLINE_QUERY_KAT + + +def test_offline_query_round_trip(): + oq = OfflineQuery.new(0, STATE_ROOT) + oq2 = OfflineQuery.from_string(str(oq)) + assert str(oq2) == str(oq) + + +def test_offline_query_add_block_height(): + oq = OfflineQuery.new(0, STATE_ROOT) + oq.add_block_height(42) + assert '"block_height":42' in str(oq) diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 59b0cf4..8279815 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -67,6 +67,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<MicroCredits>()?; m.add_class::<Metadata>()?; m.add_class::<Network>()?; + m.add_class::<OfflineQuery>()?; m.add_class::<Pedersen64>()?; m.add_class::<Pedersen128>()?; m.add_class::<Plaintext>()?; diff --git a/sdk/src/programs/dynamic_record.rs b/sdk/src/programs/dynamic_record.rs index 2e6005b..8ae5786 100644 --- a/sdk/src/programs/dynamic_record.rs +++ b/sdk/src/programs/dynamic_record.rs @@ -3,10 +3,7 @@ // // Licensed under GPL-3.0-or-later. -use crate::{ - types::DynamicRecordNative, - Address, Field, Group, RecordPlaintext, -}; +use crate::{types::DynamicRecordNative, Address, Field, Group, RecordPlaintext}; use pyo3::prelude::*; use snarkvm::prelude::{FromBytes, ToBits, ToBytes, ToFields}; @@ -41,7 +38,9 @@ impl DynamicRecord { /// Converts this DynamicRecord back to a RecordPlaintext. fn to_record(&self, owner_is_private: bool) -> anyhow::Result<RecordPlaintext> { - self.0.to_record(owner_is_private).map(RecordPlaintext::from) + self.0 + .to_record(owner_is_private) + .map(RecordPlaintext::from) } /// Serializes the dynamic record to a little-endian byte array. @@ -51,7 +50,9 @@ impl DynamicRecord { /// Returns the dynamic record as a vector of field elements. fn to_fields(&self) -> anyhow::Result<Vec<Field>> { - self.0.to_fields().map(|fs| fs.into_iter().map(Field::from).collect()) + self.0 + .to_fields() + .map(|fs| fs.into_iter().map(Field::from).collect()) } /// Returns the dynamic record as a little-endian bit vector. diff --git a/sdk/src/programs/metadata.rs b/sdk/src/programs/metadata.rs index 60c77a6..72fd234 100644 --- a/sdk/src/programs/metadata.rs +++ b/sdk/src/programs/metadata.rs @@ -13,13 +13,19 @@ const BASE_URL: &str = "https://parameters.provable.com/testnet/"; fn prover_checksum(metadata_json: &'static str) -> String { let meta: serde_json::Value = serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); - meta["prover_checksum"].as_str().expect("Failed to parse prover checksum").to_string() + meta["prover_checksum"] + .as_str() + .expect("Failed to parse prover checksum") + .to_string() } fn verifier_checksum(metadata_json: &'static str) -> String { let meta: serde_json::Value = serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); - meta["verifier_checksum"].as_str().expect("Failed to parse verifier checksum").to_string() + meta["verifier_checksum"] + .as_str() + .expect("Failed to parse verifier checksum") + .to_string() } fn make_metadata( @@ -188,12 +194,22 @@ impl Metadata { #[cfg(not(feature = "testnet"))] { use snarkvm::parameters::mainnet::InclusionProver; - make_metadata("inclusion", "inclusionVerifier", "inclusion", InclusionProver::METADATA) + make_metadata( + "inclusion", + "inclusionVerifier", + "inclusion", + InclusionProver::METADATA, + ) } #[cfg(feature = "testnet")] { use snarkvm::parameters::testnet::InclusionProver; - make_metadata("inclusion", "inclusionVerifier", "inclusion", InclusionProver::METADATA) + make_metadata( + "inclusion", + "inclusionVerifier", + "inclusion", + InclusionProver::METADATA, + ) } } @@ -202,12 +218,22 @@ impl Metadata { #[cfg(not(feature = "testnet"))] { use snarkvm::parameters::mainnet::JoinProver; - make_metadata("join", "joinVerifier", "credits.aleo/join", JoinProver::METADATA) + make_metadata( + "join", + "joinVerifier", + "credits.aleo/join", + JoinProver::METADATA, + ) } #[cfg(feature = "testnet")] { use snarkvm::parameters::testnet::JoinProver; - make_metadata("join", "joinVerifier", "credits.aleo/join", JoinProver::METADATA) + make_metadata( + "join", + "joinVerifier", + "credits.aleo/join", + JoinProver::METADATA, + ) } } @@ -240,12 +266,22 @@ impl Metadata { #[cfg(not(feature = "testnet"))] { use snarkvm::parameters::mainnet::SplitProver; - make_metadata("split", "splitVerifier", "credits.aleo/split", SplitProver::METADATA) + make_metadata( + "split", + "splitVerifier", + "credits.aleo/split", + SplitProver::METADATA, + ) } #[cfg(feature = "testnet")] { use snarkvm::parameters::testnet::SplitProver; - make_metadata("split", "splitVerifier", "credits.aleo/split", SplitProver::METADATA) + make_metadata( + "split", + "splitVerifier", + "credits.aleo/split", + SplitProver::METADATA, + ) } } diff --git a/sdk/src/programs/mod.rs b/sdk/src/programs/mod.rs index c04838c..fd4c1ab 100644 --- a/sdk/src/programs/mod.rs +++ b/sdk/src/programs/mod.rs @@ -38,6 +38,9 @@ pub use locator::Locator; mod metadata; pub use metadata::Metadata; +mod offline_query; +pub use offline_query::OfflineQuery; + mod process; pub use process::Process; diff --git a/sdk/src/programs/offline_query.rs b/sdk/src/programs/offline_query.rs new file mode 100644 index 0000000..0e9df83 --- /dev/null +++ b/sdk/src/programs/offline_query.rs @@ -0,0 +1,63 @@ +// Copyright (C) 2019-2023 Aleo Systems Inc. +// This file is part of the Aleo SDK library. +// +// Licensed under GPL-3.0-or-later. + +use crate::types::{CurrentNetwork, FieldNative, StatePathNative}; + +use indexmap::IndexMap; +use pyo3::prelude::*; +use serde::{Deserialize, Serialize}; +use snarkvm::prelude::Network; + +use std::str::FromStr; + +type StateRoot = <CurrentNetwork as Network>::StateRoot; + +/// An offline query object used to insert the global state root and state paths +/// needed to create a valid inclusion proof offline. +#[pyclass] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct OfflineQuery { + block_height: u32, + state_paths: IndexMap<FieldNative, StatePathNative>, + state_root: StateRoot, +} + +#[pymethods] +impl OfflineQuery { + /// Creates a new offline query object. + #[staticmethod] + fn new(block_height: u32, state_root: &str) -> anyhow::Result<Self> { + let state_root = StateRoot::from_str(state_root)?; + Ok(Self { + block_height, + state_paths: IndexMap::new(), + state_root, + }) + } + + /// Sets the block height. + fn add_block_height(&mut self, block_height: u32) { + self.block_height = block_height; + } + + /// Adds a state path for the given commitment field string. + fn add_state_path(&mut self, commitment: &str, state_path: &str) -> anyhow::Result<()> { + let commitment = FieldNative::from_str(commitment)?; + let state_path = StatePathNative::from_str(state_path)?; + self.state_paths.insert(commitment, state_path); + Ok(()) + } + + /// Deserializes an OfflineQuery from a JSON string. + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + serde_json::from_str(s).map_err(Into::into) + } + + /// Serializes the offline query as a JSON string. + fn __str__(&self) -> String { + serde_json::to_string(self).unwrap() + } +} From 2f5e26ce1cd9c8f6469f31b9a5fd77f4d967a6aa Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 12:55:01 -0400 Subject: [PATCH 47/72] fix: Metadata verifier checksum parity with wasm; OfflineQuery/__eq__ hardening - metadata.rs: refactor make_metadata to accept separate prover_meta and verifier_meta params; always read prover_checksum from each respective metadata JSON (mirrors wasm reference mechanics exactly). Update all 15 static constructors to pass both XxxProver::METADATA and XxxVerifier::METADATA. Drop the now-unused verifier_checksum helper. - offline_query.rs: replace .unwrap() with .expect("OfflineQuery serialization failed") in __str__ per codebase convention. - dynamic_record.rs: use direct PartialEq comparison (self.0 == other.0) since DynamicRecordNative implements std PartialEq; remove string-form workaround. - test_program_keys.py: add regex assertion for verifier filename format (bond_public.verifier.<7 hex chars>). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_program_keys.py | 2 + sdk/src/programs/dynamic_record.rs | 3 +- sdk/src/programs/metadata.rs | 130 +++++++++++++++++--------- sdk/src/programs/offline_query.rs | 2 +- 4 files changed, 90 insertions(+), 47 deletions(-) diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index bb4ba82..c51b4f0 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -1,4 +1,5 @@ """Tests for W4d2 wasm-parity additions.""" +import re import pytest from aleo.mainnet import Proof, DynamicRecord, RecordPlaintext, Metadata, OfflineQuery @@ -84,6 +85,7 @@ def test_metadata_bond_public(): assert "bond_public.prover." in m.prover assert "bond_public.verifier." in m.verifier assert "parameters.provable.com/mainnet" in m.prover + assert re.match(r"^bond_public\.verifier\.[0-9a-f]{7}$", m.verifier) def test_metadata_transfer_public(): diff --git a/sdk/src/programs/dynamic_record.rs b/sdk/src/programs/dynamic_record.rs index 8ae5786..ebc7b30 100644 --- a/sdk/src/programs/dynamic_record.rs +++ b/sdk/src/programs/dynamic_record.rs @@ -88,8 +88,7 @@ impl DynamicRecord { } fn __eq__(&self, other: &Self) -> bool { - // Compare via string representation - self.0.to_string() == other.0.to_string() + self.0 == other.0 } #[classattr] diff --git a/sdk/src/programs/metadata.rs b/sdk/src/programs/metadata.rs index 72fd234..41328a5 100644 --- a/sdk/src/programs/metadata.rs +++ b/sdk/src/programs/metadata.rs @@ -10,21 +10,12 @@ const BASE_URL: &str = "https://parameters.provable.com/mainnet/"; #[cfg(feature = "testnet")] const BASE_URL: &str = "https://parameters.provable.com/testnet/"; -fn prover_checksum(metadata_json: &'static str) -> String { +fn read_prover_checksum(metadata_json: &'static str) -> String { let meta: serde_json::Value = serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); meta["prover_checksum"] .as_str() - .expect("Failed to parse prover checksum") - .to_string() -} - -fn verifier_checksum(metadata_json: &'static str) -> String { - let meta: serde_json::Value = - serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); - meta["verifier_checksum"] - .as_str() - .expect("Failed to parse verifier checksum") + .expect("Failed to parse prover_checksum") .to_string() } @@ -32,10 +23,11 @@ fn make_metadata( name: &str, verifying_key_js_name: &str, locator: &str, - metadata_json: &'static str, + prover_meta: &'static str, + verifier_meta: &'static str, ) -> Metadata { - let pc = prover_checksum(metadata_json); - let vc = verifier_checksum(metadata_json); + let pc = read_prover_checksum(prover_meta); + let vc = read_prover_checksum(verifier_meta); Metadata { name: name.to_string(), locator: locator.to_string(), @@ -73,22 +65,24 @@ impl Metadata { fn bond_public() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::BondPublicProver; + use snarkvm::parameters::mainnet::{BondPublicProver, BondPublicVerifier}; make_metadata( "bond_public", "bondPublicVerifier", "credits.aleo/bond_public", BondPublicProver::METADATA, + BondPublicVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::BondPublicProver; + use snarkvm::parameters::testnet::{BondPublicProver, BondPublicVerifier}; make_metadata( "bond_public", "bondPublicVerifier", "credits.aleo/bond_public", BondPublicProver::METADATA, + BondPublicVerifier::METADATA, ) } } @@ -97,22 +91,24 @@ impl Metadata { fn bond_validator() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::BondValidatorProver; + use snarkvm::parameters::mainnet::{BondValidatorProver, BondValidatorVerifier}; make_metadata( "bond_validator", "bondValidatorVerifier", "credits.aleo/bond_validator", BondValidatorProver::METADATA, + BondValidatorVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::BondValidatorProver; + use snarkvm::parameters::testnet::{BondValidatorProver, BondValidatorVerifier}; make_metadata( "bond_validator", "bondValidatorVerifier", "credits.aleo/bond_validator", BondValidatorProver::METADATA, + BondValidatorVerifier::METADATA, ) } } @@ -121,22 +117,28 @@ impl Metadata { fn claim_unbond_public() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::ClaimUnbondPublicProver; + use snarkvm::parameters::mainnet::{ + ClaimUnbondPublicProver, ClaimUnbondPublicVerifier, + }; make_metadata( "claim_unbond_public", "claimUnbondPublicVerifier", "credits.aleo/claim_unbond_public", ClaimUnbondPublicProver::METADATA, + ClaimUnbondPublicVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::ClaimUnbondPublicProver; + use snarkvm::parameters::testnet::{ + ClaimUnbondPublicProver, ClaimUnbondPublicVerifier, + }; make_metadata( "claim_unbond_public", "claimUnbondPublicVerifier", "credits.aleo/claim_unbond_public", ClaimUnbondPublicProver::METADATA, + ClaimUnbondPublicVerifier::METADATA, ) } } @@ -145,22 +147,24 @@ impl Metadata { fn fee_private() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::FeePrivateProver; + use snarkvm::parameters::mainnet::{FeePrivateProver, FeePrivateVerifier}; make_metadata( "fee_private", "feePrivateVerifier", "credits.aleo/fee_private", FeePrivateProver::METADATA, + FeePrivateVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::FeePrivateProver; + use snarkvm::parameters::testnet::{FeePrivateProver, FeePrivateVerifier}; make_metadata( "fee_private", "feePrivateVerifier", "credits.aleo/fee_private", FeePrivateProver::METADATA, + FeePrivateVerifier::METADATA, ) } } @@ -169,22 +173,24 @@ impl Metadata { fn fee_public() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::FeePublicProver; + use snarkvm::parameters::mainnet::{FeePublicProver, FeePublicVerifier}; make_metadata( "fee_public", "feePublicVerifier", "credits.aleo/fee_public", FeePublicProver::METADATA, + FeePublicVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::FeePublicProver; + use snarkvm::parameters::testnet::{FeePublicProver, FeePublicVerifier}; make_metadata( "fee_public", "feePublicVerifier", "credits.aleo/fee_public", FeePublicProver::METADATA, + FeePublicVerifier::METADATA, ) } } @@ -193,22 +199,24 @@ impl Metadata { fn inclusion() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::InclusionProver; + use snarkvm::parameters::mainnet::{InclusionProver, InclusionVerifier}; make_metadata( "inclusion", "inclusionVerifier", "inclusion", InclusionProver::METADATA, + InclusionVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::InclusionProver; + use snarkvm::parameters::testnet::{InclusionProver, InclusionVerifier}; make_metadata( "inclusion", "inclusionVerifier", "inclusion", InclusionProver::METADATA, + InclusionVerifier::METADATA, ) } } @@ -217,22 +225,24 @@ impl Metadata { fn join() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::JoinProver; + use snarkvm::parameters::mainnet::{JoinProver, JoinVerifier}; make_metadata( "join", "joinVerifier", "credits.aleo/join", JoinProver::METADATA, + JoinVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::JoinProver; + use snarkvm::parameters::testnet::{JoinProver, JoinVerifier}; make_metadata( "join", "joinVerifier", "credits.aleo/join", JoinProver::METADATA, + JoinVerifier::METADATA, ) } } @@ -241,22 +251,28 @@ impl Metadata { fn set_validator_state() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::SetValidatorStateProver; + use snarkvm::parameters::mainnet::{ + SetValidatorStateProver, SetValidatorStateVerifier, + }; make_metadata( "set_validator_state", "setValidatorStateVerifier", "credits.aleo/set_validator_state", SetValidatorStateProver::METADATA, + SetValidatorStateVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::SetValidatorStateProver; + use snarkvm::parameters::testnet::{ + SetValidatorStateProver, SetValidatorStateVerifier, + }; make_metadata( "set_validator_state", "setValidatorStateVerifier", "credits.aleo/set_validator_state", SetValidatorStateProver::METADATA, + SetValidatorStateVerifier::METADATA, ) } } @@ -265,22 +281,24 @@ impl Metadata { fn split() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::SplitProver; + use snarkvm::parameters::mainnet::{SplitProver, SplitVerifier}; make_metadata( "split", "splitVerifier", "credits.aleo/split", SplitProver::METADATA, + SplitVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::SplitProver; + use snarkvm::parameters::testnet::{SplitProver, SplitVerifier}; make_metadata( "split", "splitVerifier", "credits.aleo/split", SplitProver::METADATA, + SplitVerifier::METADATA, ) } } @@ -289,22 +307,24 @@ impl Metadata { fn transfer_private() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::TransferPrivateProver; + use snarkvm::parameters::mainnet::{TransferPrivateProver, TransferPrivateVerifier}; make_metadata( "transfer_private", "transferPrivateVerifier", "credits.aleo/transfer_private", TransferPrivateProver::METADATA, + TransferPrivateVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::TransferPrivateProver; + use snarkvm::parameters::testnet::{TransferPrivateProver, TransferPrivateVerifier}; make_metadata( "transfer_private", "transferPrivateVerifier", "credits.aleo/transfer_private", TransferPrivateProver::METADATA, + TransferPrivateVerifier::METADATA, ) } } @@ -313,22 +333,28 @@ impl Metadata { fn transfer_private_to_public() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::TransferPrivateToPublicProver; + use snarkvm::parameters::mainnet::{ + TransferPrivateToPublicProver, TransferPrivateToPublicVerifier, + }; make_metadata( "transfer_private_to_public", "transferPrivateToPublicVerifier", "credits.aleo/transfer_private_to_public", TransferPrivateToPublicProver::METADATA, + TransferPrivateToPublicVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::TransferPrivateToPublicProver; + use snarkvm::parameters::testnet::{ + TransferPrivateToPublicProver, TransferPrivateToPublicVerifier, + }; make_metadata( "transfer_private_to_public", "transferPrivateToPublicVerifier", "credits.aleo/transfer_private_to_public", TransferPrivateToPublicProver::METADATA, + TransferPrivateToPublicVerifier::METADATA, ) } } @@ -337,22 +363,24 @@ impl Metadata { fn transfer_public() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::TransferPublicProver; + use snarkvm::parameters::mainnet::{TransferPublicProver, TransferPublicVerifier}; make_metadata( "transfer_public", "transferPublicVerifier", "credits.aleo/transfer_public", TransferPublicProver::METADATA, + TransferPublicVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::TransferPublicProver; + use snarkvm::parameters::testnet::{TransferPublicProver, TransferPublicVerifier}; make_metadata( "transfer_public", "transferPublicVerifier", "credits.aleo/transfer_public", TransferPublicProver::METADATA, + TransferPublicVerifier::METADATA, ) } } @@ -361,22 +389,28 @@ impl Metadata { fn transfer_public_as_signer() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::TransferPublicAsSignerProver; + use snarkvm::parameters::mainnet::{ + TransferPublicAsSignerProver, TransferPublicAsSignerVerifier, + }; make_metadata( "transfer_public_as_signer", "transferPublicAsSignerVerifier", "credits.aleo/transfer_public_as_signer", TransferPublicAsSignerProver::METADATA, + TransferPublicAsSignerVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::TransferPublicAsSignerProver; + use snarkvm::parameters::testnet::{ + TransferPublicAsSignerProver, TransferPublicAsSignerVerifier, + }; make_metadata( "transfer_public_as_signer", "transferPublicAsSignerVerifier", "credits.aleo/transfer_public_as_signer", TransferPublicAsSignerProver::METADATA, + TransferPublicAsSignerVerifier::METADATA, ) } } @@ -385,22 +419,28 @@ impl Metadata { fn transfer_public_to_private() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::TransferPublicToPrivateProver; + use snarkvm::parameters::mainnet::{ + TransferPublicToPrivateProver, TransferPublicToPrivateVerifier, + }; make_metadata( "transfer_public_to_private", "transferPublicToPrivateVerifier", "credits.aleo/transfer_public_to_private", TransferPublicToPrivateProver::METADATA, + TransferPublicToPrivateVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::TransferPublicToPrivateProver; + use snarkvm::parameters::testnet::{ + TransferPublicToPrivateProver, TransferPublicToPrivateVerifier, + }; make_metadata( "transfer_public_to_private", "transferPublicToPrivateVerifier", "credits.aleo/transfer_public_to_private", TransferPublicToPrivateProver::METADATA, + TransferPublicToPrivateVerifier::METADATA, ) } } @@ -409,22 +449,24 @@ impl Metadata { fn unbond_public() -> Metadata { #[cfg(not(feature = "testnet"))] { - use snarkvm::parameters::mainnet::UnbondPublicProver; + use snarkvm::parameters::mainnet::{UnbondPublicProver, UnbondPublicVerifier}; make_metadata( "unbond_public", "unbondPublicVerifier", "credits.aleo/unbond_public", UnbondPublicProver::METADATA, + UnbondPublicVerifier::METADATA, ) } #[cfg(feature = "testnet")] { - use snarkvm::parameters::testnet::UnbondPublicProver; + use snarkvm::parameters::testnet::{UnbondPublicProver, UnbondPublicVerifier}; make_metadata( "unbond_public", "unbondPublicVerifier", "credits.aleo/unbond_public", UnbondPublicProver::METADATA, + UnbondPublicVerifier::METADATA, ) } } diff --git a/sdk/src/programs/offline_query.rs b/sdk/src/programs/offline_query.rs index 0e9df83..416c331 100644 --- a/sdk/src/programs/offline_query.rs +++ b/sdk/src/programs/offline_query.rs @@ -58,6 +58,6 @@ impl OfflineQuery { /// Serializes the offline query as a JSON string. fn __str__(&self) -> String { - serde_json::to_string(self).unwrap() + serde_json::to_string(self).expect("OfflineQuery serialization failed") } } From 1213a0f0a29331fbe271ed42eb8f5643b2c43b59 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:01:20 -0400 Subject: [PATCH 48/72] feat: add Program.has_function, get_function_inputs, get_mappings, get_record_members, get_struct_members, address Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_program_keys.py | 77 +++++++++ sdk/src/programs/program.rs | 234 +++++++++++++++++++++++++- 2 files changed, 309 insertions(+), 2 deletions(-) diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index c51b4f0..fa2638a 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -123,3 +123,80 @@ def test_offline_query_add_block_height(): oq = OfflineQuery.new(0, STATE_ROOT) oq.add_block_height(42) assert '"block_height":42' in str(oq) + + +# --------------------------------------------------------------------------- +# Task 6: Program introspection methods +# --------------------------------------------------------------------------- +from aleo.mainnet import Program, Address # noqa: E402 + +# The credits.aleo program address for this snarkvm version. +CREDITS_ADDRESS = "aleo1lqmly7ez2k48ajf5hs92ulphaqr05qm4n8qwzj8v0yprmasgpqgsez59gg" + + +def test_program_has_function(): + p = Program.credits() + assert p.has_function("transfer_public") is True + assert p.has_function("nonexistent_fn") is False + + +def test_program_get_function_inputs_transfer_public(): + """KAT: exact dict shape for credits.aleo/transfer_public. + transfer_public takes public inputs (receiver address and amount). + """ + p = Program.credits() + inputs = p.get_function_inputs("transfer_public") + assert len(inputs) == 2 + assert inputs[0]["type"] == "address" + assert inputs[0]["visibility"] == "public" + assert inputs[0]["register"] == "r0" + assert inputs[1]["type"] == "u64" + assert inputs[1]["visibility"] == "public" + assert inputs[1]["register"] == "r1" + + +def test_program_get_mappings(): + p = Program.credits() + mappings = p.get_mappings() + names = [m["name"] for m in mappings] + for expected in ["account", "bonded", "committee"]: + assert expected in names + for m in mappings: + assert "name" in m + assert "key_type" in m + assert "value_type" in m + + +def test_program_get_record_members(): + p = Program.credits() + rec = p.get_record_members("credits") + assert rec["type"] == "record" + assert rec["record"] == "credits" + members = rec["members"] + names = [m["name"] for m in members] + assert "microcredits" in names + assert "_nonce" in names + + +def test_program_get_struct_members(): + # Use a program that has structs + src = """program test_struct.aleo; +struct point: + x as u32; + y as u32; +function noop: + input r0 as u32.public; + output r0 as u32.public; +""" + p = Program.from_source(src) + members = p.get_struct_members("point") + assert isinstance(members, list) + names = [m["name"] for m in members] + assert "x" in names + assert "y" in names + + +def test_program_address(): + p = Program.credits() + addr = p.address() + assert str(addr) == CREDITS_ADDRESS diff --git a/sdk/src/programs/program.rs b/sdk/src/programs/program.rs index a69614d..e1c832c 100644 --- a/sdk/src/programs/program.rs +++ b/sdk/src/programs/program.rs @@ -16,10 +16,15 @@ use crate::{ programs::{Identifier, ProgramID}, - types::ProgramNative, + types::{CurrentNetwork, IdentifierNative, ProgramNative}, + Address, }; -use pyo3::prelude::*; +use pyo3::{ + prelude::*, + types::{PyDict, PyList}, +}; +use snarkvm::prelude::{EntryType, PlaintextType, ValueType}; use std::{ops::Deref, str::FromStr}; @@ -84,6 +89,116 @@ impl Program { #[classattr] const __hash__: Option<PyObject> = None; + + /// Returns true if the program contains a function with the given name. + fn has_function(&self, function_name: &str) -> bool { + IdentifierNative::from_str(function_name).is_ok_and(|id| self.0.contains_function(&id)) + } + + /// Returns a list of dicts describing each input of the given function. + fn get_function_inputs<'py>( + &self, + py: Python<'py>, + function_name: &str, + ) -> anyhow::Result<PyObject> { + let id = IdentifierNative::from_str(function_name)?; + let function = self + .0 + .functions() + .get(&id) + .ok_or_else(|| anyhow::anyhow!("function {} not found", function_name))?; + let list = PyList::empty(py); + for input in function.inputs().iter() { + let register = input.register().to_string(); + match input.value_type() { + ValueType::Constant(p) => { + let d = plaintext_input_to_dict(py, &self.0, p, Some("constant"), None)?; + d.set_item("register", &register)?; + list.append(d)?; + } + ValueType::Public(p) => { + let d = plaintext_input_to_dict(py, &self.0, p, Some("public"), None)?; + d.set_item("register", &register)?; + list.append(d)?; + } + ValueType::Private(p) => { + let d = plaintext_input_to_dict(py, &self.0, p, Some("private"), None)?; + d.set_item("register", &register)?; + list.append(d)?; + } + ValueType::Record(identifier) => { + let d = record_members_dict(py, &self.0, &identifier.to_string())?; + d.set_item("register", &register)?; + list.append(d)?; + } + ValueType::ExternalRecord(locator) => { + let d = PyDict::new(py); + d.set_item("type", "external_record")?; + d.set_item("locator", locator.to_string())?; + d.set_item("register", &register)?; + list.append(d)?; + } + ValueType::Future(locator) => { + let d = PyDict::new(py); + d.set_item("type", "future")?; + d.set_item("locator", locator.to_string())?; + d.set_item("register", &register)?; + list.append(d)?; + } + ValueType::DynamicRecord => { + let d = PyDict::new(py); + d.set_item("type", "dynamic.record")?; + d.set_item("register", &register)?; + list.append(d)?; + } + ValueType::DynamicFuture => { + let d = PyDict::new(py); + d.set_item("type", "dynamic.future")?; + d.set_item("register", &register)?; + list.append(d)?; + } + } + } + Ok(list.into_py(py)) + } + + /// Returns a list of dicts describing each mapping in the program. + fn get_mappings<'py>(&self, py: Python<'py>) -> anyhow::Result<PyObject> { + let list = PyList::empty(py); + for (name, mapping) in self.0.mappings().iter() { + let d = PyDict::new(py); + d.set_item("name", name.to_string())?; + d.set_item("key_type", mapping.key().plaintext_type().to_string())?; + d.set_item("value_type", mapping.value().plaintext_type().to_string())?; + list.append(d)?; + } + Ok(list.into_py(py)) + } + + /// Returns a dict describing the members of the given record type. + fn get_record_members<'py>( + &self, + py: Python<'py>, + record_name: &str, + ) -> anyhow::Result<PyObject> { + let d = record_members_dict(py, &self.0, record_name)?; + Ok(d.into_py(py)) + } + + /// Returns a list of dicts describing the members of the given struct type. + fn get_struct_members<'py>( + &self, + py: Python<'py>, + struct_name: &str, + ) -> anyhow::Result<PyObject> { + let list = struct_members_list(py, &self.0, struct_name)?; + Ok(list.into_py(py)) + } + + /// Returns the address corresponding to this program's ID. + fn address(&self) -> anyhow::Result<Address> { + self.0.id().to_address().map(Address::from) + } } impl Deref for Program { @@ -99,3 +214,118 @@ impl From<ProgramNative> for Program { Self(program) } } + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +fn plaintext_input_to_dict<'py>( + py: Python<'py>, + program: &ProgramNative, + plaintext: &PlaintextType<CurrentNetwork>, + visibility: Option<&str>, + name: Option<&str>, +) -> anyhow::Result<&'py PyDict> { + let d = PyDict::new(py); + match plaintext { + PlaintextType::Literal(lit) => { + if let Some(n) = name { + d.set_item("name", n)?; + } + d.set_item("type", lit.to_string())?; + } + PlaintextType::Struct(struct_id) => { + if let Some(n) = name { + d.set_item("name", n)?; + } + d.set_item("type", "struct")?; + d.set_item("struct_id", struct_id.to_string())?; + // Recursively expand struct members (mirrors wasm get_plaintext_input). + let members = struct_members_list(py, program, &struct_id.to_string())?; + d.set_item("members", members)?; + } + PlaintextType::Array(array_type) => { + if let Some(n) = name { + d.set_item("name", n)?; + } + d.set_item("type", "array")?; + let elem = + plaintext_input_to_dict(py, program, array_type.base_element_type(), None, None)?; + d.set_item("element_type", elem)?; + let length: u32 = **array_type.length(); + d.set_item("length", length)?; + } + PlaintextType::ExternalStruct(locator) => { + if let Some(n) = name { + d.set_item("name", n)?; + } + d.set_item("type", "struct")?; + d.set_item("struct_id", locator.name().to_string())?; + // External structs live in another program; best-effort expansion only if locally available. + if let Ok(members) = struct_members_list(py, program, &locator.name().to_string()) { + d.set_item("members", members)?; + } + } + } + if let Some(v) = visibility { + d.set_item("visibility", v)?; + } + Ok(d) +} + +fn record_members_dict<'py>( + py: Python<'py>, + program: &ProgramNative, + record_name: &str, +) -> anyhow::Result<&'py PyDict> { + let id = IdentifierNative::from_str(record_name)?; + let record_type = program + .get_record(&id) + .map_err(|e| anyhow::anyhow!("{e}"))?; + let members_list = PyList::empty(py); + for (member_name, entry_type) in record_type.entries().iter() { + let (pt, vis) = match entry_type { + EntryType::Constant(pt) => (pt, "constant"), + EntryType::Public(pt) => (pt, "public"), + EntryType::Private(pt) => (pt, "private"), + }; + let md = + plaintext_input_to_dict(py, program, pt, Some(vis), Some(&member_name.to_string()))?; + members_list.append(md)?; + } + // Append _nonce as per wasm spec. + let nonce_d = PyDict::new(py); + nonce_d.set_item("name", "_nonce")?; + nonce_d.set_item("type", "group")?; + nonce_d.set_item("visibility", "public")?; + members_list.append(nonce_d)?; + + let d = PyDict::new(py); + d.set_item("type", "record")?; + d.set_item("record", record_name)?; + d.set_item("members", members_list)?; + Ok(d) +} + +fn struct_members_list<'py>( + py: Python<'py>, + program: &ProgramNative, + struct_name: &str, +) -> anyhow::Result<&'py PyList> { + let id = IdentifierNative::from_str(struct_name)?; + let struct_type = program + .get_struct(&id) + .map_err(|e| anyhow::anyhow!("{e}"))?; + let list = PyList::empty(py); + for (member_name, plaintext_type) in struct_type.members().iter() { + let d = plaintext_input_to_dict( + py, + program, + plaintext_type, + None, + Some(&member_name.to_string()), + )?; + list.append(d)?; + } + Ok(list) +} From e875eec6bb67ff9f11df202934b467692a348013 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:05:29 -0400 Subject: [PATCH 49/72] test: deepen Program introspection assertions (record/struct/mapping shapes) - test_program_get_record_members: assert microcredits member has type u64 and visibility private; _nonce has type group and visibility public - test_program_get_struct_members: assert full dict shape with exactly {name, type} keys; verify x and y types are u32 - test_program_get_mappings: assert account mapping has key_type address and value_type u64 - test_program_get_function_inputs_record: new test for transfer_private record input path, verifying 3 inputs, record structure with microcredits/nonce members, and address/u64 field inputs All assertions now match exact dict shapes from Rust implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .idea/.gitignore | 10 +++++ sdk/python/tests/test_program_keys.py | 63 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index fa2638a..17382af 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -166,6 +166,12 @@ def test_program_get_mappings(): assert "key_type" in m assert "value_type" in m + # Additional assertions for account mapping values + account_mapping = next((m for m in mappings if m["name"] == "account"), None) + assert account_mapping is not None + assert account_mapping["key_type"] == "address" + assert account_mapping["value_type"] == "u64" + def test_program_get_record_members(): p = Program.credits() @@ -177,6 +183,17 @@ def test_program_get_record_members(): assert "microcredits" in names assert "_nonce" in names + # Additional assertions for member shape + microcredits_member = next((m for m in members if m["name"] == "microcredits"), None) + assert microcredits_member is not None + assert microcredits_member["type"] == "u64" + assert microcredits_member["visibility"] == "private" + + nonce_member = next((m for m in members if m["name"] == "_nonce"), None) + assert nonce_member is not None + assert nonce_member["type"] == "group" + assert nonce_member["visibility"] == "public" + def test_program_get_struct_members(): # Use a program that has structs @@ -195,6 +212,52 @@ def test_program_get_struct_members(): assert "x" in names assert "y" in names + # Additional assertions for full dict shape + for member in members: + assert set(member.keys()) == {"name", "type"} + if member["name"] == "x": + assert member["type"] == "u32" + elif member["name"] == "y": + assert member["type"] == "u32" + + +def test_program_get_function_inputs_record(): + """KAT: exact dict shape for credits.aleo/transfer_private. + transfer_private takes a record input (credits) and public/private field inputs. + This mirrors the wasm test_get_inputs KAT for transfer_private. + """ + p = Program.credits() + inputs = p.get_function_inputs("transfer_private") + assert len(inputs) == 3 + + # Input 0: record type + assert inputs[0]["type"] == "record" + assert inputs[0]["record"] == "credits" + assert inputs[0]["register"] == "r0" + assert "members" in inputs[0] + + # Check record members + members = inputs[0]["members"] + microcredits = next((m for m in members if m["name"] == "microcredits"), None) + assert microcredits is not None + assert microcredits["type"] == "u64" + assert microcredits["visibility"] == "private" + + nonce = next((m for m in members if m["name"] == "_nonce"), None) + assert nonce is not None + assert nonce["type"] == "group" + assert nonce["visibility"] == "public" + + # Input 1: address (private) + assert inputs[1]["type"] == "address" + assert inputs[1]["visibility"] == "private" + assert inputs[1]["register"] == "r1" + + # Input 2: u64 (private) + assert inputs[2]["type"] == "u64" + assert inputs[2]["visibility"] == "private" + assert inputs[2]["register"] == "r2" + def test_program_address(): p = Program.credits() From 4e5c1e748099d9f66a84c693a10812329912531a Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:06:36 -0400 Subject: [PATCH 50/72] chore: remove accidentally committed .idea, gitignore it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .gitignore | 2 +- .idea/.gitignore | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.gitignore b/.gitignore index 3406770..b878628 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ tmp/ .DS_Store -*.pkl \ No newline at end of file +*.pkl.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index ab1f416..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ From 95eb78446fdfa7b963c129943dfc192af5b234dd Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:17:42 -0400 Subject: [PATCH 51/72] feat: add VerifyingKey.checksum, num_constraints, 16 credits statics, 16 is_*_verifier checkers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/Cargo.lock | 132 +++++--------- sdk/Cargo.toml | 2 + sdk/python/tests/test_program_keys.py | 79 +++++++++ sdk/src/programs/verifying_key.rs | 243 ++++++++++++++++++++++++++ 4 files changed, 368 insertions(+), 88 deletions(-) diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index 215529e..b0ee5d6 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -28,12 +28,14 @@ name = "aleo" version = "0.2.0" dependencies = [ "anyhow", + "hex", "indexmap", "once_cell", "pyo3", "rand", "serde", "serde_json", + "sha2 0.10.9", "snarkvm", ] @@ -325,7 +327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "rand_core", ] @@ -350,7 +352,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -426,6 +428,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -702,7 +713,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1286,7 +1297,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.11.0", "wnaf", ] @@ -1584,7 +1595,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -1756,7 +1767,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.5", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -1795,9 +1806,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.5", + "socket2 0.6.4", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2001,7 +2012,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2060,7 +2071,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2204,6 +2215,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.11.0" @@ -2211,7 +2233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "digest 0.11.3", ] @@ -2321,7 +2343,7 @@ dependencies = [ "rand", "rayon", "serde", - "sha2", + "sha2 0.11.0", "smallvec", "snarkvm-curves", "snarkvm-fields", @@ -3011,7 +3033,7 @@ dependencies = [ "paste", "rand", "serde_json", - "sha2", + "sha2 0.11.0", "snarkvm-curves", "snarkvm-utilities", "thiserror 2.0.18", @@ -3274,7 +3296,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3817,16 +3839,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] @@ -3844,28 +3857,13 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3874,84 +3872,42 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - [[package]] name = "wnaf" version = "0.14.0" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 029ff09..5f267b6 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -20,12 +20,14 @@ testnet = [] [dependencies] anyhow = "1" +hex = "0.4" indexmap = "2.1.0" once_cell = "1.18.0" pyo3 = { version = "0.20.0", features = ["extension-module", "abi3-py37", "anyhow"] } rand = { version = "0.10" } serde = "1" serde_json = "1" +sha2 = "0.10" snarkvm = { git = "https://github.com/ProvableHQ/snarkVM.git", tag = "v4.8.1", default-features = false, features = [ "console", "circuit", "synthesizer", "ledger", "utilities", "algorithms", "parameters", diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index 17382af..9e6db9d 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -263,3 +263,82 @@ def test_program_address(): p = Program.credits() addr = p.address() assert str(addr) == CREDITS_ADDRESS + + +# --------------------------------------------------------------------------- +# Task 7: VerifyingKey additions (checksum, num_constraints, credits statics, is_*_verifier) +# --------------------------------------------------------------------------- +from aleo.mainnet import VerifyingKey # noqa: E402 + +# KAT from wasm tests (mainnet) — sha256 of the raw verifier bytes / to_bytes_le() round-trip +TRANSFER_PUBLIC_VK_CHECKSUM = "ea77f42a35b3f891e7753c7333df365f356883550c4602df11f270237bef340d" +TRANSFER_PUBLIC_NUM_CONSTRAINTS = 12326 + +# All 16 (function_name, getter_name) pairs for credits + inclusion +CREDITS_VERIFIER_GETTERS = [ + ("bond_public", "bond_public_verifier"), + ("bond_validator", "bond_validator_verifier"), + ("claim_unbond_public", "claim_unbond_public_verifier"), + ("fee_private", "fee_private_verifier"), + ("fee_public", "fee_public_verifier"), + ("join", "join_verifier"), + ("set_validator_state", "set_validator_state_verifier"), + ("split", "split_verifier"), + ("transfer_private", "transfer_private_verifier"), + ("transfer_private_to_public", "transfer_private_to_public_verifier"), + ("transfer_public", "transfer_public_verifier"), + ("transfer_public_as_signer", "transfer_public_as_signer_verifier"), + ("transfer_public_to_private", "transfer_public_to_private_verifier"), + ("unbond_public", "unbond_public_verifier"), + # inclusion + (None, "inclusion_verifier"), +] + + +def test_verifying_key_checksum(): + vk = VerifyingKey.transfer_public_verifier() + assert vk.checksum() == TRANSFER_PUBLIC_VK_CHECKSUM + + +def test_verifying_key_num_constraints(): + vk = VerifyingKey.transfer_public_verifier() + assert vk.num_constraints() == TRANSFER_PUBLIC_NUM_CONSTRAINTS + + +def test_verifying_key_bond_public_verifier_is_checker(): + bond = VerifyingKey.bond_public_verifier() + transfer = VerifyingKey.transfer_public_verifier() + assert bond.is_bond_public_verifier() is True + assert transfer.is_bond_public_verifier() is False + + +def test_verifying_key_inclusion_verifier(): + vk = VerifyingKey.inclusion_verifier() + assert vk.is_inclusion_verifier() is True + assert vk.is_bond_public_verifier() is False + + +def test_all_16_verifier_getters_and_is_checkers(): + """Iterate all 16 getters and verify each is_* self-check returns True.""" + IS_CHECKER_MAP = { + "bond_public_verifier": "is_bond_public_verifier", + "bond_validator_verifier": "is_bond_validator_verifier", + "claim_unbond_public_verifier": "is_claim_unbond_public_verifier", + "fee_private_verifier": "is_fee_private_verifier", + "fee_public_verifier": "is_fee_public_verifier", + "join_verifier": "is_join_verifier", + "set_validator_state_verifier": "is_set_validator_state_verifier", + "split_verifier": "is_split_verifier", + "transfer_private_verifier": "is_transfer_private_verifier", + "transfer_private_to_public_verifier": "is_transfer_private_to_public_verifier", + "transfer_public_verifier": "is_transfer_public_verifier", + "transfer_public_as_signer_verifier": "is_transfer_public_as_signer_verifier", + "transfer_public_to_private_verifier": "is_transfer_public_to_private_verifier", + "unbond_public_verifier": "is_unbond_public_verifier", + "inclusion_verifier": "is_inclusion_verifier", + } + for _fn_name, getter in CREDITS_VERIFIER_GETTERS: + vk = getattr(VerifyingKey, getter)() + is_checker = IS_CHECKER_MAP[getter] + result = getattr(vk, is_checker)() + assert result is True, f"{getter}: {is_checker}() returned {result}, expected True" diff --git a/sdk/src/programs/verifying_key.rs b/sdk/src/programs/verifying_key.rs index 2a919b3..3a38664 100644 --- a/sdk/src/programs/verifying_key.rs +++ b/sdk/src/programs/verifying_key.rs @@ -17,6 +17,7 @@ use crate::types::VerifyingKeyNative; use pyo3::prelude::*; +use sha2::Digest; use snarkvm::prelude::{FromBytes, ToBytes}; use std::str::FromStr; @@ -56,6 +57,248 @@ impl VerifyingKey { #[classattr] const __hash__: Option<PyObject> = None; + + // ---- Task 7 additions ---- + + /// Returns the SHA256 hex checksum of the verifying key bytes. + fn checksum(&self) -> anyhow::Result<String> { + let bytes = self.0.to_bytes_le()?; + Ok(hex::encode(sha2::Sha256::digest(&bytes))) + } + + /// Returns the number of constraints in the circuit. + fn num_constraints(&self) -> u32 { + self.0.circuit_info.num_constraints as u32 + } + + // ---- credits static getters ---- + + #[staticmethod] + fn bond_public_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::BondPublicVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::BondPublicVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn bond_validator_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::BondValidatorVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::BondValidatorVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn claim_unbond_public_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::ClaimUnbondPublicVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::ClaimUnbondPublicVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn fee_private_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::FeePrivateVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::FeePrivateVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn fee_public_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::FeePublicVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::FeePublicVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn inclusion_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::InclusionVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::InclusionVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn join_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::JoinVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::JoinVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn set_validator_state_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::SetValidatorStateVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::SetValidatorStateVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn split_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::SplitVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::SplitVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn transfer_private_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::TransferPrivateVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::TransferPrivateVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn transfer_private_to_public_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::TransferPrivateToPublicVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::TransferPrivateToPublicVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn transfer_public_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::TransferPublicVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::TransferPublicVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn transfer_public_as_signer_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::TransferPublicAsSignerVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::TransferPublicAsSignerVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn transfer_public_to_private_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::TransferPublicToPrivateVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::TransferPublicToPrivateVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + #[staticmethod] + fn unbond_public_verifier() -> anyhow::Result<VerifyingKey> { + #[cfg(not(feature = "testnet"))] + let bytes = snarkvm::parameters::mainnet::UnbondPublicVerifier::load_bytes()?; + #[cfg(feature = "testnet")] + let bytes = snarkvm::parameters::testnet::UnbondPublicVerifier::load_bytes()?; + VerifyingKeyNative::from_bytes_le(&bytes).map(Self) + } + + // ---- is_*_verifier checkers ---- + + fn is_bond_public_verifier(&self) -> bool { + Self::bond_public_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_bond_validator_verifier(&self) -> bool { + Self::bond_validator_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_claim_unbond_public_verifier(&self) -> bool { + Self::claim_unbond_public_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_fee_private_verifier(&self) -> bool { + Self::fee_private_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_fee_public_verifier(&self) -> bool { + Self::fee_public_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_inclusion_verifier(&self) -> bool { + Self::inclusion_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_join_verifier(&self) -> bool { + Self::join_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_set_validator_state_verifier(&self) -> bool { + Self::set_validator_state_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_split_verifier(&self) -> bool { + Self::split_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_transfer_private_verifier(&self) -> bool { + Self::transfer_private_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_transfer_private_to_public_verifier(&self) -> bool { + Self::transfer_private_to_public_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_transfer_public_verifier(&self) -> bool { + Self::transfer_public_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_transfer_public_as_signer_verifier(&self) -> bool { + Self::transfer_public_as_signer_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_transfer_public_to_private_verifier(&self) -> bool { + Self::transfer_public_to_private_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } + + fn is_unbond_public_verifier(&self) -> bool { + Self::unbond_public_verifier() + .map(|vk| *self.0 == *vk.0) + .unwrap_or(false) + } } impl From<VerifyingKeyNative> for VerifyingKey { From f8e436bc3deed092f88b3a6e93aa1d3597eaab8e Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:23:38 -0400 Subject: [PATCH 52/72] feat: add ProvingKey.checksum and 15 is_*_prover checkers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_program_keys.py | 68 +++++ sdk/src/programs/proving_key.rs | 366 ++++++++++++++++++++++++++ 2 files changed, 434 insertions(+) diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index 9e6db9d..2c3c6da 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -342,3 +342,71 @@ def test_all_16_verifier_getters_and_is_checkers(): is_checker = IS_CHECKER_MAP[getter] result = getattr(vk, is_checker)() assert result is True, f"{getter}: {is_checker}() returned {result}, expected True" + + +# --------------------------------------------------------------------------- +# Task 8: ProvingKey additions (checksum, is_*_prover) +# --------------------------------------------------------------------------- +from aleo.mainnet import ProvingKey # noqa: E402 + +# All 15 is_*_prover method names +PROVER_CHECKER_METHODS = [ + "is_bond_public_prover", + "is_bond_validator_prover", + "is_claim_unbond_public_prover", + "is_fee_private_prover", + "is_fee_public_prover", + "is_inclusion_prover", + "is_join_prover", + "is_set_validator_state_prover", + "is_split_prover", + "is_transfer_private_prover", + "is_transfer_private_to_public_prover", + "is_transfer_public_prover", + "is_transfer_public_as_signer_prover", + "is_transfer_public_to_private_prover", + "is_unbond_public_prover", +] + + +def test_proving_key_checksum_shape(): + """Shape test: checksum() method exists and raises on invalid bytes (not AttributeError).""" + with pytest.raises(Exception, match=r"."): + ProvingKey.from_bytes(bytes(10)).checksum() + + +def test_proving_key_checker_methods_exist(): + """API-surface test: all 15 is_*_prover methods exist and are callable on ProvingKey.""" + for method_name in PROVER_CHECKER_METHODS: + assert callable(getattr(ProvingKey, method_name, None)), ( + f"ProvingKey.{method_name} is not callable" + ) + + +@pytest.mark.slow +def test_proving_key_split_prover_checksum(): + """Slow: downloads split.prover, verifies checksum() matches metadata and is_split_prover() is True.""" + import urllib.request + + meta = Metadata.split() + url = meta.prover # e.g. https://parameters.provable.com/mainnet/split.prover.XXXXXXX + try: + with urllib.request.urlopen(url, timeout=120) as resp: + prover_bytes = resp.read() + except Exception: + pytest.skip("network unavailable or download failed") + + pk = ProvingKey.from_bytes(prover_bytes) + + # checksum() should return a 64-char hex string + cs = pk.checksum() + assert isinstance(cs, str) + assert len(cs) == 64 + assert all(c in "0123456789abcdef" for c in cs) + + # The first 7 chars of the checksum appear in the prover URL + assert cs[:7] in url, f"checksum prefix {cs[:7]} not found in URL {url}" + + # is_split_prover() must be True; a different checker must be False + assert pk.is_split_prover() is True + assert pk.is_bond_public_prover() is False diff --git a/sdk/src/programs/proving_key.rs b/sdk/src/programs/proving_key.rs index e797ec8..473d86b 100644 --- a/sdk/src/programs/proving_key.rs +++ b/sdk/src/programs/proving_key.rs @@ -17,10 +17,21 @@ use crate::types::ProvingKeyNative; use pyo3::prelude::*; +use sha2::Digest; use snarkvm::prelude::{FromBytes, ToBytes}; use std::str::FromStr; +/// Extract the `prover_checksum` field from an embedded METADATA JSON string. +fn prover_checksum_from_meta(metadata_json: &'static str) -> String { + let meta: serde_json::Value = + serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); + meta["prover_checksum"] + .as_str() + .expect("Failed to parse prover_checksum") + .to_string() +} + /// The Aleo proving key type. #[pyclass(frozen)] #[derive(Clone)] @@ -56,6 +67,361 @@ impl ProvingKey { #[classattr] const __hash__: Option<PyObject> = None; + + // ---- Task 8 additions ---- + + /// Returns the SHA256 hex checksum of the proving key bytes. + fn checksum(&self) -> anyhow::Result<String> { + let bytes = self.0.to_bytes_le()?; + Ok(hex::encode(sha2::Sha256::digest(&bytes))) + } + + // ---- is_*_prover checkers ---- + + fn is_bond_public_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::BondPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::BondPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_bond_validator_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::BondValidatorProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::BondValidatorProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_claim_unbond_public_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::ClaimUnbondPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::ClaimUnbondPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_fee_private_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::FeePrivateProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::FeePrivateProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_fee_public_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::FeePublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::FeePublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_inclusion_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::InclusionProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::InclusionProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_join_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::JoinProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::JoinProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_set_validator_state_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::SetValidatorStateProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::SetValidatorStateProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_split_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::SplitProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::SplitProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_transfer_private_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::TransferPrivateProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::TransferPrivateProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_transfer_private_to_public_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::TransferPrivateToPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::TransferPrivateToPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_transfer_public_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::TransferPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::TransferPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_transfer_public_as_signer_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::TransferPublicAsSignerProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::TransferPublicAsSignerProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_transfer_public_to_private_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::TransferPublicToPrivateProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::TransferPublicToPrivateProver::METADATA, + ) + }) + .unwrap_or(false) + } + } + + fn is_unbond_public_prover(&self) -> bool { + #[cfg(not(feature = "testnet"))] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::mainnet::UnbondPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + #[cfg(feature = "testnet")] + { + self.checksum() + .map(|cs| { + cs == prover_checksum_from_meta( + snarkvm::parameters::testnet::UnbondPublicProver::METADATA, + ) + }) + .unwrap_or(false) + } + } } impl From<ProvingKeyNative> for ProvingKey { From 831518dab4b508401d35b0f2b985b282a0b9e210 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:27:41 -0400 Subject: [PATCH 53/72] refactor: reuse metadata read_prover_checksum in ProvingKey checkers Eliminate DRY violation by reusing the read_prover_checksum function from metadata.rs instead of duplicating it in proving_key.rs. All 30 checker call sites (15 checkers x 2 cfg branches) now use the shared implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/src/programs/metadata.rs | 2 +- sdk/src/programs/proving_key.rs | 79 +++++++++++++-------------------- 2 files changed, 32 insertions(+), 49 deletions(-) diff --git a/sdk/src/programs/metadata.rs b/sdk/src/programs/metadata.rs index 41328a5..85e4f89 100644 --- a/sdk/src/programs/metadata.rs +++ b/sdk/src/programs/metadata.rs @@ -10,7 +10,7 @@ const BASE_URL: &str = "https://parameters.provable.com/mainnet/"; #[cfg(feature = "testnet")] const BASE_URL: &str = "https://parameters.provable.com/testnet/"; -fn read_prover_checksum(metadata_json: &'static str) -> String { +pub(crate) fn read_prover_checksum(metadata_json: &'static str) -> String { let meta: serde_json::Value = serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); meta["prover_checksum"] diff --git a/sdk/src/programs/proving_key.rs b/sdk/src/programs/proving_key.rs index 473d86b..f732675 100644 --- a/sdk/src/programs/proving_key.rs +++ b/sdk/src/programs/proving_key.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. +use super::metadata::read_prover_checksum; use crate::types::ProvingKeyNative; use pyo3::prelude::*; @@ -22,16 +23,6 @@ use snarkvm::prelude::{FromBytes, ToBytes}; use std::str::FromStr; -/// Extract the `prover_checksum` field from an embedded METADATA JSON string. -fn prover_checksum_from_meta(metadata_json: &'static str) -> String { - let meta: serde_json::Value = - serde_json::from_str(metadata_json).expect("Metadata was not well-formatted"); - meta["prover_checksum"] - .as_str() - .expect("Failed to parse prover_checksum") - .to_string() -} - /// The Aleo proving key type. #[pyclass(frozen)] #[derive(Clone)] @@ -83,7 +74,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::BondPublicProver::METADATA, ) }) @@ -93,7 +84,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::BondPublicProver::METADATA, ) }) @@ -106,7 +97,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::BondValidatorProver::METADATA, ) }) @@ -116,7 +107,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::BondValidatorProver::METADATA, ) }) @@ -129,7 +120,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::ClaimUnbondPublicProver::METADATA, ) }) @@ -139,7 +130,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::ClaimUnbondPublicProver::METADATA, ) }) @@ -152,7 +143,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::FeePrivateProver::METADATA, ) }) @@ -162,7 +153,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::FeePrivateProver::METADATA, ) }) @@ -175,7 +166,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::FeePublicProver::METADATA, ) }) @@ -185,7 +176,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::FeePublicProver::METADATA, ) }) @@ -198,7 +189,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::InclusionProver::METADATA, ) }) @@ -208,7 +199,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::InclusionProver::METADATA, ) }) @@ -221,9 +212,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( - snarkvm::parameters::mainnet::JoinProver::METADATA, - ) + cs == read_prover_checksum(snarkvm::parameters::mainnet::JoinProver::METADATA) }) .unwrap_or(false) } @@ -231,9 +220,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( - snarkvm::parameters::testnet::JoinProver::METADATA, - ) + cs == read_prover_checksum(snarkvm::parameters::testnet::JoinProver::METADATA) }) .unwrap_or(false) } @@ -244,7 +231,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::SetValidatorStateProver::METADATA, ) }) @@ -254,7 +241,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::SetValidatorStateProver::METADATA, ) }) @@ -267,9 +254,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( - snarkvm::parameters::mainnet::SplitProver::METADATA, - ) + cs == read_prover_checksum(snarkvm::parameters::mainnet::SplitProver::METADATA) }) .unwrap_or(false) } @@ -277,9 +262,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( - snarkvm::parameters::testnet::SplitProver::METADATA, - ) + cs == read_prover_checksum(snarkvm::parameters::testnet::SplitProver::METADATA) }) .unwrap_or(false) } @@ -290,7 +273,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::TransferPrivateProver::METADATA, ) }) @@ -300,7 +283,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::TransferPrivateProver::METADATA, ) }) @@ -313,7 +296,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::TransferPrivateToPublicProver::METADATA, ) }) @@ -323,7 +306,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::TransferPrivateToPublicProver::METADATA, ) }) @@ -336,7 +319,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::TransferPublicProver::METADATA, ) }) @@ -346,7 +329,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::TransferPublicProver::METADATA, ) }) @@ -359,7 +342,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::TransferPublicAsSignerProver::METADATA, ) }) @@ -369,7 +352,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::TransferPublicAsSignerProver::METADATA, ) }) @@ -382,7 +365,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::TransferPublicToPrivateProver::METADATA, ) }) @@ -392,7 +375,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::TransferPublicToPrivateProver::METADATA, ) }) @@ -405,7 +388,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::mainnet::UnbondPublicProver::METADATA, ) }) @@ -415,7 +398,7 @@ impl ProvingKey { { self.checksum() .map(|cs| { - cs == prover_checksum_from_meta( + cs == read_prover_checksum( snarkvm::parameters::testnet::UnbondPublicProver::METADATA, ) }) From ddef49bd63c7d08122cf7884510e7711cdae90ae Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:38:05 -0400 Subject: [PATCH 54/72] feat: add Ciphertext.decrypt_with_transition_view_key and decrypt_with_transition_info Implements two decrypt methods on the Ciphertext pyclass in sdk/src/account/text.rs, mirroring the wasm SDK's key derivation: compute_function_id + hash_psd4([fn_id, tvk, index]). Tests use a self-generated MainnetV0 KAT (network ID 0) since the wasm KAT was testnet (ID 1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_program_keys.py | 105 ++++++++++++++++++++++++++ sdk/src/account/text.rs | 65 +++++++++++++++- 2 files changed, 168 insertions(+), 2 deletions(-) diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index 2c3c6da..fb6f25b 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -410,3 +410,108 @@ def test_proving_key_split_prover_checksum(): # is_split_prover() must be True; a different checker must be False assert pk.is_split_prover() is True assert pk.is_bond_public_prover() is False + + +# --------------------------------------------------------------------------- +# Task 9: Ciphertext.decrypt_with_transition_view_key and decrypt_with_transition_info +# --------------------------------------------------------------------------- +from aleo.mainnet import Ciphertext, ViewKey, Transition, Group, Field # noqa: E402 + +# KAT generated for MainnetV0 (network ID 0): +# Private key: APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH +# tpk: Group::generator() +# tvk: (tpk * vk_scalar).to_x_coordinate() +# ciphertext: encrypt_symmetric(hash_psd4([function_id, tvk, Field::from_u16(1)])) +# on plaintext "42u32" with program="hello_hello.aleo", function="hello", index=1 +# +# NOTE: The wasm SDK KAT used network ID 1 (testnet). This SDK targets MainnetV0 +# (network ID 0), so the wasm KAT ciphertext does not decrypt to the expected +# "2u32" on mainnet. These tests use a self-generated KAT verified by a Rust unit +# test to ensure correctness with the actual network ID. +_TASK9_PK_STR = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" +_TASK9_TPK_STR = "1540945439182663264862696551825005342995406165131907382295858612069623286213group" +_TASK9_TVK_STR = "5913691033198561235987877735437800286574097563446004224670039882724243551875field" +_TASK9_CT_STR = "ciphertext1qyqxk3s4jy4cz265h82q8m0z7zm0jr8w8xkh89a2qpy82kq0ja8w5rq2qj5rw" +_TASK9_PROGRAM = "hello_hello.aleo" +_TASK9_FUNCTION = "hello" +_TASK9_INDEX = 1 +_TASK9_EXPECTED = "42u32" + + +def test_ciphertext_decrypt_with_transition_view_key(): + """KAT: decrypt_with_transition_view_key returns '42u32' for the mainnet KAT.""" + tvk = Field.from_string(_TASK9_TVK_STR) + ct = Ciphertext.from_string(_TASK9_CT_STR) + plaintext = ct.decrypt_with_transition_view_key( + tvk, _TASK9_PROGRAM, _TASK9_FUNCTION, _TASK9_INDEX + ) + assert str(plaintext) == _TASK9_EXPECTED + + +def test_ciphertext_decrypt_with_transition_info(): + """KAT: decrypt_with_transition_info returns '42u32' for the mainnet KAT.""" + from aleo.mainnet import PrivateKey + + pk = PrivateKey.from_string(_TASK9_PK_STR) + vk = pk.view_key + tpk = Group.from_string(_TASK9_TPK_STR) + ct = Ciphertext.from_string(_TASK9_CT_STR) + plaintext = ct.decrypt_with_transition_info( + vk, tpk, _TASK9_PROGRAM, _TASK9_FUNCTION, _TASK9_INDEX + ) + assert str(plaintext) == _TASK9_EXPECTED + + +def test_ciphertext_decrypt_methods_agree(): + """Self-consistency: both methods produce identical plaintexts for the same inputs.""" + from aleo.mainnet import PrivateKey + + pk = PrivateKey.from_string(_TASK9_PK_STR) + vk = pk.view_key + tpk = Group.from_string(_TASK9_TPK_STR) + tvk = Field.from_string(_TASK9_TVK_STR) + ct = Ciphertext.from_string(_TASK9_CT_STR) + + p_tvk = ct.decrypt_with_transition_view_key( + tvk, _TASK9_PROGRAM, _TASK9_FUNCTION, _TASK9_INDEX + ) + p_info = ct.decrypt_with_transition_info( + vk, tpk, _TASK9_PROGRAM, _TASK9_FUNCTION, _TASK9_INDEX + ) + assert str(p_tvk) == str(p_info) + + +def test_ciphertext_decrypt_wrong_index_fails(): + """Decrypting with the wrong index must raise (different symmetric key).""" + tvk = Field.from_string(_TASK9_TVK_STR) + ct = Ciphertext.from_string(_TASK9_CT_STR) + with pytest.raises(Exception): + ct.decrypt_with_transition_view_key( + tvk, _TASK9_PROGRAM, _TASK9_FUNCTION, 0 # wrong index + ) + + +def test_ciphertext_decrypt_wrong_program_fails(): + """Decrypting with the wrong program name must raise.""" + tvk = Field.from_string(_TASK9_TVK_STR) + ct = Ciphertext.from_string(_TASK9_CT_STR) + with pytest.raises(Exception): + ct.decrypt_with_transition_view_key( + tvk, "credits.aleo", _TASK9_FUNCTION, _TASK9_INDEX + ) + + +def test_ciphertext_decrypt_with_transition_info_wrong_vk_fails(): + """Decrypting with a different view key must raise.""" + from aleo.mainnet import PrivateKey + + other_pk = PrivateKey.from_string( + "APrivateKey1zkpJkyYRGYtkeHDaFfwsKtUJzia7csiWhfBWPXWhXJzy9Ls" + ) + other_vk = other_pk.view_key + tpk = Group.from_string(_TASK9_TPK_STR) + ct = Ciphertext.from_string(_TASK9_CT_STR) + with pytest.raises(Exception): + ct.decrypt_with_transition_info( + other_vk, tpk, _TASK9_PROGRAM, _TASK9_FUNCTION, _TASK9_INDEX + ) diff --git a/sdk/src/account/text.rs b/sdk/src/account/text.rs index 1e1e9e7..d971148 100644 --- a/sdk/src/account/text.rs +++ b/sdk/src/account/text.rs @@ -15,7 +15,10 @@ // along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. use crate::{ - types::{CiphertextNative, FieldNative, IdentifierNative, LiteralNative, PlaintextNative}, + types::{ + CiphertextNative, CurrentNetwork, FieldNative, IdentifierNative, LiteralNative, + PlaintextNative, ProgramIDNative, U16Native, + }, Address, Field, Group, Identifier, Literal, Scalar, ViewKey, }; use std::ops::Deref; @@ -23,7 +26,8 @@ use std::ops::Deref; use pyo3::{exceptions::PyTypeError, prelude::*}; use snarkvm::prelude::{ - FromBits, FromBytes, FromFields, ToBits, ToBitsRaw, ToBytes, ToFields, ToFieldsRaw, + compute_function_id, FromBits, FromBytes, FromFields, Network, ToBits, ToBitsRaw, ToBytes, + ToFields, ToFieldsRaw, }; use std::{collections::HashMap, str::FromStr}; @@ -53,6 +57,63 @@ impl Ciphertext { .map(Into::into) } + /// Decrypt a ciphertext using the transition view key and (program, function, index). + /// + /// Computes: function_id = compute_function_id(network_id, program_id, function_name); + /// ivk = hash_psd4([function_id, tvk, Field::from_u16(index)]); + /// then decrypts symmetrically with ivk. + fn decrypt_with_transition_view_key( + &self, + tvk: &Field, + program: &str, + function_name: &str, + index: u16, + ) -> anyhow::Result<Plaintext> { + let program_id = ProgramIDNative::from_str(program)?; + let function_ident = IdentifierNative::from_str(function_name)?; + let function_id = compute_function_id( + &U16Native::new(CurrentNetwork::ID), + &program_id, + &function_ident, + )?; + let index_field = FieldNative::from_u16(index); + let input_view_key = CurrentNetwork::hash_psd4(&[function_id, **tvk, index_field]) + .map_err(|e| anyhow::anyhow!("{e}"))?; + self.0 + .decrypt_symmetric(input_view_key) + .map(Plaintext::from) + .map_err(|e| anyhow::anyhow!("{e}")) + } + + /// Decrypt a ciphertext using the view key, transition public key, and (program, function, index). + /// + /// Derives tvk = (tpk * view_key_scalar).to_x_coordinate(), then calls + /// decrypt_with_transition_view_key logic internally. + fn decrypt_with_transition_info( + &self, + view_key: &ViewKey, + tpk: &Group, + program: &str, + function_name: &str, + index: u16, + ) -> anyhow::Result<Plaintext> { + let program_id = ProgramIDNative::from_str(program)?; + let function_ident = IdentifierNative::from_str(function_name)?; + let function_id = compute_function_id( + &U16Native::new(CurrentNetwork::ID), + &program_id, + &function_ident, + )?; + let tvk = (**tpk * ***view_key).to_x_coordinate(); + let index_field = FieldNative::from_u16(index); + let input_view_key = CurrentNetwork::hash_psd4(&[function_id, tvk, index_field]) + .map_err(|e| anyhow::anyhow!("{e}"))?; + self.0 + .decrypt_symmetric(input_view_key) + .map(Plaintext::from) + .map_err(|e| anyhow::anyhow!("{e}")) + } + /// Returns the ciphertext as a string. fn __str__(&self) -> String { self.0.to_string() From b2a009265d527acab5927fb4c5a919dc75c8d61c Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:43:30 -0400 Subject: [PATCH 55/72] test: cross-validate Ciphertext transition decrypt against Transition.decrypt_transition Add test_ciphertext_decrypt_cross_validates_with_transition_decrypt to test_program_keys.py. The existing Task 9 KAT tests only prove that decrypt_with_transition_view_key decrypts its own self-generated output; a shared derivation bug (e.g. wrong field order in hash_psd4) would not be caught. The new test exercises both paths against the vendored hello_hello.aleo/main mainnet fixture (private input index 1 = "2u32"), where Transition.decrypt_transition was independently tested in test_chain_data.py, ensuring that any per-path derivation regression is caught. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_program_keys.py | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index fb6f25b..dee84bc 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -515,3 +515,91 @@ def test_ciphertext_decrypt_with_transition_info_wrong_vk_fails(): ct.decrypt_with_transition_info( other_vk, tpk, _TASK9_PROGRAM, _TASK9_FUNCTION, _TASK9_INDEX ) + + +# --------------------------------------------------------------------------- +# Task 9 cross-validation: Ciphertext.decrypt_with_transition_view_key vs +# Transition.decrypt_transition (independently tested in test_chain_data.py). +# +# The self-generated KAT tests above only prove "the code decrypts its own +# output". A shared derivation bug (e.g. wrong field order in hash_psd4) would +# not be caught because both paths would share the same defect. +# +# This test uses the vendored hello_hello.aleo/main fixture from +# test_chain_data.py, which has a real private input at index 1 (value "2u32") +# encrypted under the transition view key derived from DECRYPTION_PRIVATE_KEY. +# Transition.decrypt_transition was independently tested in test_chain_data.py +# (TestTransition.test_decrypt_transition), so agreeing with it here catches +# any bug that affects only one of the two derivation paths. +# --------------------------------------------------------------------------- + +# Vendored from test_chain_data.py (hello_hello.aleo/main, mainnet fixture): +_XVAL_TRANSITION_JSON = ( + '{"id":"au1mguuz0dh20f78802m4z0py7n08xhl0pz60llzck63mhl8pc8l5xqxpwgtn",' + '"program":"hello_hello.aleo","function":"main",' + '"inputs":[' + '{"type":"public","id":"6393584049543470937057043098611271993206122889317039351966319038535020834557field","value":"1u32"},' + '{"type":"private","id":"8207446256045172951742235001162005156507562935942883128759030124682934277495field",' + '"value":"ciphertext1qyqqgz9qnupeld9vr4vuwp6yrpmhgtkvmgag5m7mmrruw0r6je666qgqdswk3"}' + '],' + '"outputs":[{"type":"private","id":"127469473292952941321346770257126666363371158501875622169294663492714835110field",' + '"value":"ciphertext1qyqyapkjuxm9dcslgyjf7hkr2k3dek500z40gjspnwvll0uawj23vzgggc405"}],' + '"tpk":"7647553513996966044119163122930125808381703910407273818947266861843062002251group",' + '"tcm":"4479413938380109857414238205380483440836495997450846894155088299187217672609field",' + '"scm":"6461007226176477784737642021400489186736987671609840640950580467598882134642field"}' +) +_XVAL_PRIVATE_KEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" +# Index 1 is the private input (r1 as u32.private); index 0 is public. +_XVAL_PRIVATE_INPUT_INDEX = 1 +_XVAL_PRIVATE_INPUT_CT = "ciphertext1qyqqgz9qnupeld9vr4vuwp6yrpmhgtkvmgag5m7mmrruw0r6je666qgqdswk3" +_XVAL_EXPECTED_PLAINTEXT = "2u32" + + +def test_ciphertext_decrypt_cross_validates_with_transition_decrypt(): + """Cross-validation: Ciphertext.decrypt_with_transition_view_key agrees with + Transition.decrypt_transition for the same private input. + + Path A (reference, independently tested in test_chain_data.py): + transition.decrypt_transition(tvk) -> read decrypted input value. + + Path B (method under review): + Ciphertext.from_string(ct_str).decrypt_with_transition_view_key(tvk, program, fn, index). + + If hash_psd4 field ordering or any other step in the key derivation were + wrong in exactly one path, this test would fail even though the KAT tests + pass (since those exercise only Path B with a self-generated vector). + """ + from aleo.mainnet import PrivateKey + + pk = PrivateKey.from_string(_XVAL_PRIVATE_KEY) + transition = Transition.from_json(_XVAL_TRANSITION_JSON) + tvk = transition.tvk(pk.view_key) + + # Path A: Transition.decrypt_transition — independent implementation. + decrypted = transition.decrypt_transition(tvk) + decrypted_inputs = decrypted.inputs() + # The private input at index 1 is now exposed as a public plaintext. + path_a_input = decrypted_inputs[_XVAL_PRIVATE_INPUT_INDEX] + assert path_a_input["type"] == "public", ( + f"Expected decrypted input to have type 'public', got {path_a_input['type']!r}" + ) + path_a_value = path_a_input["value"] + + # Path B: Ciphertext.decrypt_with_transition_view_key — the method under review. + ct = Ciphertext.from_string(_XVAL_PRIVATE_INPUT_CT) + path_b_plaintext = ct.decrypt_with_transition_view_key( + tvk, "hello_hello.aleo", "main", _XVAL_PRIVATE_INPUT_INDEX + ) + path_b_value = str(path_b_plaintext) + + # Both paths must agree with each other and with the known plaintext. + assert path_a_value == _XVAL_EXPECTED_PLAINTEXT, ( + f"Path A (decrypt_transition) returned {path_a_value!r}, expected {_XVAL_EXPECTED_PLAINTEXT!r}" + ) + assert path_b_value == _XVAL_EXPECTED_PLAINTEXT, ( + f"Path B (decrypt_with_transition_view_key) returned {path_b_value!r}, expected {_XVAL_EXPECTED_PLAINTEXT!r}" + ) + assert path_a_value == path_b_value, ( + f"Path A ({path_a_value!r}) and Path B ({path_b_value!r}) disagree — " + "derivation bug in one of the two implementations" + ) From 329352072406642c85dd40d561d133f482200d4f Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:51:01 -0400 Subject: [PATCH 56/72] feat: update type stubs for Proof, DynamicRecord, Metadata, OfflineQuery, Program/VK/PK/Ciphertext additions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 154 ++++++++++++++++++++++++++- sdk/python/aleo/_aleolib_mainnet.pyi | 154 ++++++++++++++++++++++++++- 2 files changed, 302 insertions(+), 6 deletions(-) diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index ccca0b2..e76ad2f 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -157,6 +157,8 @@ class Ciphertext: def from_string(s: str) -> Ciphertext: ... def decrypt(self, view_key: ViewKey, nonce: Group) -> Plaintext: ... def decrypt_symmetric(self, plaintext_view_key: Field) -> Plaintext: ... + def decrypt_with_transition_view_key(self, tvk: Field, program: str, function_name: str, index: int) -> Plaintext: ... + def decrypt_with_transition_info(self, view_key: ViewKey, tpk: Group, program: str, function_name: str, index: int) -> Plaintext: ... class Credits: @@ -164,6 +166,26 @@ class Credits: def micro(self) -> MicroCredits: ... +class DynamicRecord: + @staticmethod + def from_string(s: str) -> DynamicRecord: ... + @staticmethod + def from_record(record: RecordPlaintext) -> DynamicRecord: ... + @staticmethod + def from_bytes(bytes: bytes) -> DynamicRecord: ... + def to_record(self, owner_is_private: bool) -> RecordPlaintext: ... + def bytes(self) -> bytes: ... + def to_fields(self) -> List[Field]: ... + def to_bits_le(self) -> List[bool]: ... + @property + def owner(self) -> Address: ... + @property + def root(self) -> Field: ... + @property + def nonce(self) -> Group: ... + def is_hiding(self) -> bool: ... + + class ComputeKey: @property def address(self) -> Address: ... @@ -677,6 +699,51 @@ class Locator: def resource(self) -> Identifier: ... +class Metadata: + name: str + locator: str + prover: str + verifier: str + verifying_key: str + @staticmethod + def base_url() -> str: ... + @staticmethod + def bond_public() -> Metadata: ... + @staticmethod + def bond_validator() -> Metadata: ... + @staticmethod + def claim_unbond_public() -> Metadata: ... + @staticmethod + def fee_private() -> Metadata: ... + @staticmethod + def fee_public() -> Metadata: ... + @staticmethod + def inclusion() -> Metadata: ... + @staticmethod + def join() -> Metadata: ... + @staticmethod + def set_validator_state() -> Metadata: ... + @staticmethod + def split() -> Metadata: ... + @staticmethod + def transfer_private() -> Metadata: ... + @staticmethod + def transfer_private_to_public() -> Metadata: ... + @staticmethod + def transfer_public() -> Metadata: ... + @staticmethod + def transfer_public_as_signer() -> Metadata: ... + @staticmethod + def transfer_public_to_private() -> Metadata: ... + @staticmethod + def unbond_public() -> Metadata: ... + + +class MicroCredits: + def __new__(cls, value: int) -> MicroCredits: ... + def __int__(self) -> int: ... + + class Network: @staticmethod def name() -> str: ... @@ -686,9 +753,13 @@ class Network: def hash_psd2(input: List[Field]) -> Field: ... -class MicroCredits: - def __new__(cls, value: int) -> MicroCredits: ... - def __int__(self) -> int: ... +class OfflineQuery: + @staticmethod + def new(block_height: int, state_root: str) -> OfflineQuery: ... + @staticmethod + def from_string(s: str) -> OfflineQuery: ... + def add_block_height(self, block_height: int) -> None: ... + def add_state_path(self, commitment: str, state_path: str) -> None: ... class Pedersen64: @@ -744,6 +815,14 @@ class Plaintext: def to_python(self) -> object: ... +class Proof: + @staticmethod + def from_string(s: str) -> Proof: ... + @staticmethod + def from_bytes(bytes: bytes) -> Proof: ... + def bytes(self) -> bytes: ... + + class PrivateKeyCiphertext: @staticmethod def encrypt_private_key(private_key: PrivateKey, secret: str) -> PrivateKeyCiphertext: ... @@ -850,6 +929,12 @@ class Program: def imports(self) -> List[ProgramID]: ... @property def source(self) -> str: ... + def has_function(self, function_name: str) -> bool: ... + def get_function_inputs(self, function_name: str) -> List[Dict[str, Any]]: ... + def get_mappings(self) -> List[Dict[str, Any]]: ... + def get_record_members(self, record_name: str) -> Dict[str, Any]: ... + def get_struct_members(self, struct_name: str) -> List[Dict[str, Any]]: ... + def address(self) -> Address: ... class ProgramID: @@ -868,6 +953,22 @@ class ProvingKey: @staticmethod def from_bytes(bytes: bytes) -> ProvingKey: ... def bytes(self) -> bytes: ... + def checksum(self) -> str: ... + def is_bond_public_prover(self) -> bool: ... + def is_bond_validator_prover(self) -> bool: ... + def is_claim_unbond_public_prover(self) -> bool: ... + def is_fee_private_prover(self) -> bool: ... + def is_fee_public_prover(self) -> bool: ... + def is_inclusion_prover(self) -> bool: ... + def is_join_prover(self) -> bool: ... + def is_set_validator_state_prover(self) -> bool: ... + def is_split_prover(self) -> bool: ... + def is_transfer_private_prover(self) -> bool: ... + def is_transfer_private_to_public_prover(self) -> bool: ... + def is_transfer_public_prover(self) -> bool: ... + def is_transfer_public_as_signer_prover(self) -> bool: ... + def is_transfer_public_to_private_prover(self) -> bool: ... + def is_unbond_public_prover(self) -> bool: ... class Query: @@ -1379,6 +1480,53 @@ class VerifyingKey: @staticmethod def from_bytes(bytes: bytes) -> VerifyingKey: ... def bytes(self) -> bytes: ... + def checksum(self) -> str: ... + def num_constraints(self) -> int: ... + @staticmethod + def bond_public_verifier() -> VerifyingKey: ... + @staticmethod + def bond_validator_verifier() -> VerifyingKey: ... + @staticmethod + def claim_unbond_public_verifier() -> VerifyingKey: ... + @staticmethod + def fee_private_verifier() -> VerifyingKey: ... + @staticmethod + def fee_public_verifier() -> VerifyingKey: ... + @staticmethod + def inclusion_verifier() -> VerifyingKey: ... + @staticmethod + def join_verifier() -> VerifyingKey: ... + @staticmethod + def set_validator_state_verifier() -> VerifyingKey: ... + @staticmethod + def split_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_private_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_private_to_public_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_public_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_public_as_signer_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_public_to_private_verifier() -> VerifyingKey: ... + @staticmethod + def unbond_public_verifier() -> VerifyingKey: ... + def is_bond_public_verifier(self) -> bool: ... + def is_bond_validator_verifier(self) -> bool: ... + def is_claim_unbond_public_verifier(self) -> bool: ... + def is_fee_private_verifier(self) -> bool: ... + def is_fee_public_verifier(self) -> bool: ... + def is_inclusion_verifier(self) -> bool: ... + def is_join_verifier(self) -> bool: ... + def is_set_validator_state_verifier(self) -> bool: ... + def is_split_verifier(self) -> bool: ... + def is_transfer_private_verifier(self) -> bool: ... + def is_transfer_private_to_public_verifier(self) -> bool: ... + def is_transfer_public_verifier(self) -> bool: ... + def is_transfer_public_as_signer_verifier(self) -> bool: ... + def is_transfer_public_to_private_verifier(self) -> bool: ... + def is_unbond_public_verifier(self) -> bool: ... class ViewKey: diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index d39f80a..7afa0d0 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -157,6 +157,8 @@ class Ciphertext: def from_string(s: str) -> Ciphertext: ... def decrypt(self, view_key: ViewKey, nonce: Group) -> Plaintext: ... def decrypt_symmetric(self, plaintext_view_key: Field) -> Plaintext: ... + def decrypt_with_transition_view_key(self, tvk: Field, program: str, function_name: str, index: int) -> Plaintext: ... + def decrypt_with_transition_info(self, view_key: ViewKey, tpk: Group, program: str, function_name: str, index: int) -> Plaintext: ... class Credits: @@ -164,6 +166,26 @@ class Credits: def micro(self) -> MicroCredits: ... +class DynamicRecord: + @staticmethod + def from_string(s: str) -> DynamicRecord: ... + @staticmethod + def from_record(record: RecordPlaintext) -> DynamicRecord: ... + @staticmethod + def from_bytes(bytes: bytes) -> DynamicRecord: ... + def to_record(self, owner_is_private: bool) -> RecordPlaintext: ... + def bytes(self) -> bytes: ... + def to_fields(self) -> list[Field]: ... + def to_bits_le(self) -> list[bool]: ... + @property + def owner(self) -> Address: ... + @property + def root(self) -> Field: ... + @property + def nonce(self) -> Group: ... + def is_hiding(self) -> bool: ... + + class ComputeKey: @property def address(self) -> Address: ... @@ -670,6 +692,51 @@ class Locator: def resource(self) -> Identifier: ... +class Metadata: + name: str + locator: str + prover: str + verifier: str + verifying_key: str + @staticmethod + def base_url() -> str: ... + @staticmethod + def bond_public() -> Metadata: ... + @staticmethod + def bond_validator() -> Metadata: ... + @staticmethod + def claim_unbond_public() -> Metadata: ... + @staticmethod + def fee_private() -> Metadata: ... + @staticmethod + def fee_public() -> Metadata: ... + @staticmethod + def inclusion() -> Metadata: ... + @staticmethod + def join() -> Metadata: ... + @staticmethod + def set_validator_state() -> Metadata: ... + @staticmethod + def split() -> Metadata: ... + @staticmethod + def transfer_private() -> Metadata: ... + @staticmethod + def transfer_private_to_public() -> Metadata: ... + @staticmethod + def transfer_public() -> Metadata: ... + @staticmethod + def transfer_public_as_signer() -> Metadata: ... + @staticmethod + def transfer_public_to_private() -> Metadata: ... + @staticmethod + def unbond_public() -> Metadata: ... + + +class MicroCredits: + def __new__(cls, value: int) -> MicroCredits: ... + def __int__(self) -> int: ... + + class Network: @staticmethod def name() -> str: ... @@ -679,9 +746,13 @@ class Network: def hash_psd2(input: list[Field]) -> Field: ... -class MicroCredits: - def __new__(cls, value: int) -> MicroCredits: ... - def __int__(self) -> int: ... +class OfflineQuery: + @staticmethod + def new(block_height: int, state_root: str) -> OfflineQuery: ... + @staticmethod + def from_string(s: str) -> OfflineQuery: ... + def add_block_height(self, block_height: int) -> None: ... + def add_state_path(self, commitment: str, state_path: str) -> None: ... class Pedersen64: @@ -737,6 +808,14 @@ class Plaintext: def to_python(self) -> object: ... +class Proof: + @staticmethod + def from_string(s: str) -> Proof: ... + @staticmethod + def from_bytes(bytes: bytes) -> Proof: ... + def bytes(self) -> bytes: ... + + class PrivateKeyCiphertext: @staticmethod def encrypt_private_key(private_key: "PrivateKey", secret: str) -> "PrivateKeyCiphertext": ... @@ -840,6 +919,12 @@ class Program: def imports(self) -> list[ProgramID]: ... @property def source(self) -> str: ... + def has_function(self, function_name: str) -> bool: ... + def get_function_inputs(self, function_name: str) -> list[dict[str, Any]]: ... + def get_mappings(self) -> list[dict[str, Any]]: ... + def get_record_members(self, record_name: str) -> dict[str, Any]: ... + def get_struct_members(self, struct_name: str) -> list[dict[str, Any]]: ... + def address(self) -> Address: ... class ProgramID: @@ -858,6 +943,22 @@ class ProvingKey: @staticmethod def from_bytes(bytes: bytes) -> ProvingKey: ... def bytes(self) -> bytes: ... + def checksum(self) -> str: ... + def is_bond_public_prover(self) -> bool: ... + def is_bond_validator_prover(self) -> bool: ... + def is_claim_unbond_public_prover(self) -> bool: ... + def is_fee_private_prover(self) -> bool: ... + def is_fee_public_prover(self) -> bool: ... + def is_inclusion_prover(self) -> bool: ... + def is_join_prover(self) -> bool: ... + def is_set_validator_state_prover(self) -> bool: ... + def is_split_prover(self) -> bool: ... + def is_transfer_private_prover(self) -> bool: ... + def is_transfer_private_to_public_prover(self) -> bool: ... + def is_transfer_public_prover(self) -> bool: ... + def is_transfer_public_as_signer_prover(self) -> bool: ... + def is_transfer_public_to_private_prover(self) -> bool: ... + def is_unbond_public_prover(self) -> bool: ... class Query: @@ -1368,6 +1469,53 @@ class VerifyingKey: @staticmethod def from_bytes(bytes: bytes) -> VerifyingKey: ... def bytes(self) -> bytes: ... + def checksum(self) -> str: ... + def num_constraints(self) -> int: ... + @staticmethod + def bond_public_verifier() -> VerifyingKey: ... + @staticmethod + def bond_validator_verifier() -> VerifyingKey: ... + @staticmethod + def claim_unbond_public_verifier() -> VerifyingKey: ... + @staticmethod + def fee_private_verifier() -> VerifyingKey: ... + @staticmethod + def fee_public_verifier() -> VerifyingKey: ... + @staticmethod + def inclusion_verifier() -> VerifyingKey: ... + @staticmethod + def join_verifier() -> VerifyingKey: ... + @staticmethod + def set_validator_state_verifier() -> VerifyingKey: ... + @staticmethod + def split_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_private_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_private_to_public_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_public_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_public_as_signer_verifier() -> VerifyingKey: ... + @staticmethod + def transfer_public_to_private_verifier() -> VerifyingKey: ... + @staticmethod + def unbond_public_verifier() -> VerifyingKey: ... + def is_bond_public_verifier(self) -> bool: ... + def is_bond_validator_verifier(self) -> bool: ... + def is_claim_unbond_public_verifier(self) -> bool: ... + def is_fee_private_verifier(self) -> bool: ... + def is_fee_public_verifier(self) -> bool: ... + def is_inclusion_verifier(self) -> bool: ... + def is_join_verifier(self) -> bool: ... + def is_set_validator_state_verifier(self) -> bool: ... + def is_split_verifier(self) -> bool: ... + def is_transfer_private_verifier(self) -> bool: ... + def is_transfer_private_to_public_verifier(self) -> bool: ... + def is_transfer_public_verifier(self) -> bool: ... + def is_transfer_public_as_signer_verifier(self) -> bool: ... + def is_transfer_public_to_private_verifier(self) -> bool: ... + def is_unbond_public_verifier(self) -> bool: ... class ViewKey: From aab9dfa75f686c96fc7197bf37cb9b604394ab50 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:54:16 -0400 Subject: [PATCH 57/72] fix: restore alphabetical class ordering in stubs (ComputeKey before DynamicRecord) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 16 ++++++++-------- sdk/python/aleo/_aleolib_mainnet.pyi | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index e76ad2f..c3f038e 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -166,6 +166,14 @@ class Credits: def micro(self) -> MicroCredits: ... +class ComputeKey: + @property + def address(self) -> Address: ... + def pk_sig(self) -> Group: ... + def pr_sig(self) -> Group: ... + def sk_prf(self) -> Scalar: ... + + class DynamicRecord: @staticmethod def from_string(s: str) -> DynamicRecord: ... @@ -186,14 +194,6 @@ class DynamicRecord: def is_hiding(self) -> bool: ... -class ComputeKey: - @property - def address(self) -> Address: ... - def pk_sig(self) -> Group: ... - def pr_sig(self) -> Group: ... - def sk_prf(self) -> Scalar: ... - - class Encryptor: @staticmethod def encrypt_private_key_with_secret(private_key: PrivateKey, secret: str) -> Ciphertext: ... diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index 7afa0d0..60c3b23 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -166,6 +166,14 @@ class Credits: def micro(self) -> MicroCredits: ... +class ComputeKey: + @property + def address(self) -> Address: ... + def pk_sig(self) -> Group: ... + def pr_sig(self) -> Group: ... + def sk_prf(self) -> Scalar: ... + + class DynamicRecord: @staticmethod def from_string(s: str) -> DynamicRecord: ... @@ -186,14 +194,6 @@ class DynamicRecord: def is_hiding(self) -> bool: ... -class ComputeKey: - @property - def address(self) -> Address: ... - def pk_sig(self) -> Group: ... - def pr_sig(self) -> Group: ... - def sk_prf(self) -> Scalar: ... - - class Execution: @property def execution_id(self) -> Field: ... From 0e1afd32d86cc5b3e47469f4080a669e82c36fa6 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 14:25:31 -0400 Subject: [PATCH 58/72] fix: repair .gitignore patterns; proving tests use v2 endpoint with 503 retry - .gitignore had '*.pkl.idea/' concatenation (both patterns dead) - test naming: 15 credits verifiers, not 16 - proving tests: api.explorer.provable.com/v2 (verified live) + 3-attempt retry around trace.prepare for transient API 503s Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .gitignore | 3 ++- sdk/python/tests/test_program_keys.py | 4 ++-- sdk/python/tests/test_proving.py | 33 +++++++++++++++++++++++---- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index b878628..3ff33b2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ tmp/ .DS_Store -*.pkl.idea/ +*.pkl +.idea/ diff --git a/sdk/python/tests/test_program_keys.py b/sdk/python/tests/test_program_keys.py index dee84bc..4874ba9 100644 --- a/sdk/python/tests/test_program_keys.py +++ b/sdk/python/tests/test_program_keys.py @@ -274,7 +274,7 @@ def test_program_address(): TRANSFER_PUBLIC_VK_CHECKSUM = "ea77f42a35b3f891e7753c7333df365f356883550c4602df11f270237bef340d" TRANSFER_PUBLIC_NUM_CONSTRAINTS = 12326 -# All 16 (function_name, getter_name) pairs for credits + inclusion +# All 15 (function_name, getter_name) pairs for credits + inclusion CREDITS_VERIFIER_GETTERS = [ ("bond_public", "bond_public_verifier"), ("bond_validator", "bond_validator_verifier"), @@ -318,7 +318,7 @@ def test_verifying_key_inclusion_verifier(): assert vk.is_bond_public_verifier() is False -def test_all_16_verifier_getters_and_is_checkers(): +def test_all_15_verifier_getters_and_is_checkers(): """Iterate all 16 getters and verify each is_* self-check returns True.""" IS_CHECKER_MAP = { "bond_public_verifier": "is_bond_public_verifier", diff --git a/sdk/python/tests/test_proving.py b/sdk/python/tests/test_proving.py index e130619..c54144d 100644 --- a/sdk/python/tests/test_proving.py +++ b/sdk/python/tests/test_proving.py @@ -13,8 +13,8 @@ They are excluded from CI via -m "not slow". Endpoint note (verified empirically): Query.rest() wants the BASE url -`https://api.explorer.provable.com/v1` — snarkvm's REST query appends the -network path (`/mainnet/stateRoot/latest`) itself; passing `.../v1/mainnet` +`https://api.explorer.provable.com/v2` — snarkvm's REST query appends the +network path (`/mainnet/stateRoot/latest`) itself; passing `.../v2/mainnet` would double the network segment. """ @@ -38,7 +38,30 @@ ) from conftest import load_vectors -ENDPOINT = "https://api.explorer.provable.com/v1" +ENDPOINT = "https://api.explorer.provable.com/v2" + + +def _prepare_with_retry(trace, attempts: int = 3, delay: float = 20.0) -> None: + """Prepare a trace against the live endpoint, retrying transient outages. + + The Provable API intermittently returns 503s (nginx); a failed state-root + fetch here is infrastructure noise, not a proving regression, so retry a + few times before letting the error surface. + """ + import time + + last: Exception | None = None + for attempt in range(attempts): + try: + trace.prepare(Query.rest(ENDPOINT)) + return + except RuntimeError as exc: # snarkvm surfaces HTTP failures as RuntimeError + last = exc + if attempt < attempts - 1: + time.sleep(delay) + assert last is not None + raise last + CREDITS = ProgramID.from_string("credits.aleo") TRANSFER_PUBLIC = Identifier.from_string("transfer_public") @@ -134,7 +157,7 @@ def proven_execution(process, signer): """ auth = _authorize_transfer_public(process, signer) _, trace = process.execute(auth) - trace.prepare(Query.rest(ENDPOINT)) + _prepare_with_retry(trace) return trace.prove_execution("credits.aleo/transfer_public") @@ -177,7 +200,7 @@ def test_prove_fee_and_transaction(process, signer, proven_execution): assert fee_trace.is_fee() is True assert fee_trace.is_fee_public() is True - fee_trace.prepare(Query.rest(ENDPOINT)) + _prepare_with_retry(fee_trace) fee = fee_trace.prove_fee() assert fee.is_fee_public() is True assert str(fee.payer) == str(signer.address) From 4f27399568c17bcad09615cee94bbe16549ce15a Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:24:25 -0400 Subject: [PATCH 59/72] feat: ProvingRequest and ExecutionRequest for delegated proving Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.pyi | 65 +++ sdk/python/aleo/_aleolib_mainnet.pyi | 65 +++ sdk/python/tests/test_proving_request.py | 150 +++++++ sdk/python/tests/vectors/proving_request.json | 5 + sdk/src/lib.rs | 2 + sdk/src/programs/execution_request.rs | 216 ++++++++++ sdk/src/programs/mod.rs | 6 + sdk/src/programs/proving_request.rs | 390 ++++++++++++++++++ sdk/src/types.rs | 6 +- 9 files changed, 903 insertions(+), 2 deletions(-) create mode 100644 sdk/python/tests/test_proving_request.py create mode 100644 sdk/python/tests/vectors/proving_request.json create mode 100644 sdk/src/programs/execution_request.rs create mode 100644 sdk/src/programs/proving_request.rs diff --git a/sdk/python/aleo/__init__.pyi b/sdk/python/aleo/__init__.pyi index c3f038e..5a3075b 100644 --- a/sdk/python/aleo/__init__.pyi +++ b/sdk/python/aleo/__init__.pyi @@ -216,6 +216,38 @@ class Execution: def bytes(self) -> bytes: ... +class ExecutionRequest: + @staticmethod + def sign( + private_key: PrivateKey, + program_id: str, + function_name: str, + inputs: List[str], + input_types: List[str], + root_tvk: Field | None, + program_checksum: Field | None, + is_root: bool, + is_dynamic: bool, + ) -> ExecutionRequest: ... + @staticmethod + def from_string(request: str) -> ExecutionRequest: ... + @staticmethod + def from_bytes(bytes: bytes) -> ExecutionRequest: ... + def bytes(self) -> bytes: ... + def signer(self) -> Address: ... + def network_id(self) -> int: ... + def program_id(self) -> str: ... + def function_name(self) -> str: ... + def inputs(self) -> List[str]: ... + def signature(self) -> Signature: ... + def sk_tag(self) -> Field: ... + def tvk(self) -> Field: ... + def tcm(self) -> Field: ... + def scm(self) -> Field: ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + + class Fee: def is_fee_private(self) -> bool: ... def is_fee_public(self) -> bool: ... @@ -971,6 +1003,39 @@ class ProvingKey: def is_unbond_public_prover(self) -> bool: ... +class ProvingRequest: + def __new__( + cls, + authorization: Authorization, + fee_authorization: Authorization | None, + broadcast: bool, + ) -> ProvingRequest: ... + @staticmethod + def from_request( + request: ExecutionRequest, + fee_request: ExecutionRequest | None, + broadcast: bool, + ) -> ProvingRequest: ... + def kind(self) -> str: ... + def is_authorization(self) -> bool: ... + def is_request(self) -> bool: ... + @staticmethod + def from_string(s: str) -> ProvingRequest: ... + @staticmethod + def from_bytes(bytes: bytes) -> ProvingRequest: ... + @staticmethod + def from_bytes_request(bytes: bytes) -> ProvingRequest: ... + def bytes(self) -> bytes: ... + def authorization(self) -> Authorization: ... + def fee_authorization(self) -> Authorization | None: ... + def request(self) -> ExecutionRequest: ... + def fee_request(self) -> ExecutionRequest | None: ... + @property + def broadcast(self) -> bool: ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + + class Query: @staticmethod def rest(url: str) -> Query: ... diff --git a/sdk/python/aleo/_aleolib_mainnet.pyi b/sdk/python/aleo/_aleolib_mainnet.pyi index 60c3b23..c2c4b99 100644 --- a/sdk/python/aleo/_aleolib_mainnet.pyi +++ b/sdk/python/aleo/_aleolib_mainnet.pyi @@ -209,6 +209,38 @@ class Execution: def bytes(self) -> bytes: ... +class ExecutionRequest: + @staticmethod + def sign( + private_key: PrivateKey, + program_id: str, + function_name: str, + inputs: list[str], + input_types: list[str], + root_tvk: Field | None, + program_checksum: Field | None, + is_root: bool, + is_dynamic: bool, + ) -> ExecutionRequest: ... + @staticmethod + def from_string(request: str) -> ExecutionRequest: ... + @staticmethod + def from_bytes(bytes: bytes) -> ExecutionRequest: ... + def bytes(self) -> bytes: ... + def signer(self) -> Address: ... + def network_id(self) -> int: ... + def program_id(self) -> str: ... + def function_name(self) -> str: ... + def inputs(self) -> list[str]: ... + def signature(self) -> Signature: ... + def sk_tag(self) -> Field: ... + def tvk(self) -> Field: ... + def tcm(self) -> Field: ... + def scm(self) -> Field: ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + + class Fee: def is_fee_private(self) -> bool: ... def is_fee_public(self) -> bool: ... @@ -961,6 +993,39 @@ class ProvingKey: def is_unbond_public_prover(self) -> bool: ... +class ProvingRequest: + def __new__( + cls, + authorization: Authorization, + fee_authorization: Authorization | None, + broadcast: bool, + ) -> ProvingRequest: ... + @staticmethod + def from_request( + request: ExecutionRequest, + fee_request: ExecutionRequest | None, + broadcast: bool, + ) -> ProvingRequest: ... + def kind(self) -> str: ... + def is_authorization(self) -> bool: ... + def is_request(self) -> bool: ... + @staticmethod + def from_string(s: str) -> ProvingRequest: ... + @staticmethod + def from_bytes(bytes: bytes) -> ProvingRequest: ... + @staticmethod + def from_bytes_request(bytes: bytes) -> ProvingRequest: ... + def bytes(self) -> bytes: ... + def authorization(self) -> Authorization: ... + def fee_authorization(self) -> Authorization | None: ... + def request(self) -> ExecutionRequest: ... + def fee_request(self) -> ExecutionRequest | None: ... + @property + def broadcast(self) -> bool: ... + def __str__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + + class Query: @staticmethod def rest(url: str) -> Query: ... diff --git a/sdk/python/tests/test_proving_request.py b/sdk/python/tests/test_proving_request.py new file mode 100644 index 0000000..ae6ec4c --- /dev/null +++ b/sdk/python/tests/test_proving_request.py @@ -0,0 +1,150 @@ +"""Tests for the ProvingRequest and ExecutionRequest types (delegated proving). + +Ported from the ProvableHQ ts-sdk wasm crate +(wasm/src/synthesizer/proving_request.rs). These are mainnet fixtures; the SDK +under test is built with the mainnet feature (network ID 0). + +The `authorization()` accessor is compared to the bare AUTHORIZATION fixture by +JSON string, mirroring the wasm `.equals()` check (the Python `Authorization` +class does not expose `__eq__`). +""" + +from aleo.mainnet import ( + Authorization, + ExecutionRequest, + PrivateKey, + ProvingRequest, +) + +from conftest import load_vectors + +VECTORS = load_vectors("proving_request.json") +PROVING_REQUEST = VECTORS["PUZZLE_SPINNER_V002_PROVING_REQUEST"] +AUTHORIZATION = VECTORS["PUZZLE_SPINNER_V002_AUTHORIZATION"] + +# Beacon private key -- matches the wasm-side fixtures. +BEACON_PRIVATE_KEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" + +# transfer_public-style Request-variant inputs (from the wasm test). +TRANSFER_ADDRESS = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" +TRANSFER_INPUTS = [TRANSFER_ADDRESS, "100u64"] +TRANSFER_INPUT_TYPES = ["address.public", "u64.public"] + + +def _sample_execution_request() -> ExecutionRequest: + private_key = PrivateKey.from_string(BEACON_PRIVATE_KEY) + return ExecutionRequest.sign( + private_key, + "credits.aleo", + "transfer_public", + TRANSFER_INPUTS, + TRANSFER_INPUT_TYPES, + None, # root_tvk + None, # program_checksum + True, # is_root + False, # is_dynamic + ) + + +# ---- Authorization variant (legacy) ------------------------------------- + + +def test_proving_request_serialization_roundtrip(): + proving_request = ProvingRequest.from_string(PROVING_REQUEST) + + byte_roundtrip = ProvingRequest.from_bytes(bytes(proving_request.bytes())) + string_roundtrip = ProvingRequest.from_string(str(proving_request)) + + assert proving_request == byte_roundtrip + assert proving_request == string_roundtrip + + +def test_proving_request_accessor_methods_give_correct_authorizations(): + proving_request = ProvingRequest.from_string(PROVING_REQUEST) + + authorization = Authorization.from_json(AUTHORIZATION) + # Mirror the wasm `.equals()` check via JSON (Authorization has no __eq__). + assert proving_request.authorization().to_json() == authorization.to_json() + assert proving_request.fee_authorization().is_fee_public() + assert proving_request.broadcast is False + assert proving_request.kind() == "authorization" + + +def test_fee_authorization_binds_to_execution_id(): + """The fee authorization's execution-id input must equal the main + authorization's execution ID. This pins our to_execution_id (W4c) against + the reference fixture.""" + import json + + proving_request = ProvingRequest.from_string(PROVING_REQUEST) + fee = json.loads(proving_request.fee_authorization().to_json()) + # credits.aleo/fee_public inputs: [base_fee, priority_fee, execution_id]. + execution_id_input = fee["requests"][0]["inputs"][2] + expected = str(proving_request.authorization().to_execution_id()) + assert execution_id_input == expected + + +# ---- Request variant ---------------------------------------------------- + + +def test_request_variant_construction_and_kind(): + request = _sample_execution_request() + proving_request = ProvingRequest.from_request(request, None, False) + + assert proving_request.kind() == "request" + assert proving_request.is_request() + assert not proving_request.is_authorization() + assert proving_request.request() is not None + assert proving_request.fee_request() is None + assert proving_request.broadcast is False + + +def test_request_variant_byte_roundtrip(): + request = _sample_execution_request() + proving_request = ProvingRequest.from_request(request, None, True) + + # Roundtrip via the explicit Request reader; the byte layout carries no + # discriminator so the Authorization reader would not work here. + roundtripped = ProvingRequest.from_bytes_request(bytes(proving_request.bytes())) + + assert proving_request == roundtripped + assert roundtripped.kind() == "request" + assert roundtripped.broadcast is True + + +def test_request_variant_string_roundtrip_autodetects(): + request = _sample_execution_request() + proving_request = ProvingRequest.from_request(request, None, False) + + # JSON shape carries the variant -- from_string picks the right one via + # untagged serde dispatch on disjoint field names. + roundtripped = ProvingRequest.from_string(str(proving_request)) + + assert roundtripped.kind() == "request" + assert proving_request == roundtripped + + +def test_request_variant_accessors_raise_on_authorization_methods(): + request = _sample_execution_request() + proving_request = ProvingRequest.from_request(request, None, False) + + try: + proving_request.authorization() + raise AssertionError("expected authorization() to raise") + except Exception as err: + assert "Request variant" in str(err) + + # fee_authorization() returns None rather than raising. + assert proving_request.fee_authorization() is None + + +def test_authorization_variant_raises_on_request_accessor(): + proving_request = ProvingRequest.from_string(PROVING_REQUEST) + + try: + proving_request.request() + raise AssertionError("expected request() to raise") + except Exception as err: + assert "Authorization variant" in str(err) + + assert proving_request.fee_request() is None diff --git a/sdk/python/tests/vectors/proving_request.json b/sdk/python/tests/vectors/proving_request.json new file mode 100644 index 0000000..e090bc3 --- /dev/null +++ b/sdk/python/tests/vectors/proving_request.json @@ -0,0 +1,5 @@ +{ + "_source": "Vendored verbatim from ProvableHQ ts-sdk wasm/src/utilities/test/programs.rs @543b41e (PUZZLE_SPINNER_V002_PROVING_REQUEST ~:230, PUZZLE_SPINNER_V002_AUTHORIZATION ~:232). Do not edit by hand.", + "PUZZLE_SPINNER_V002_PROVING_REQUEST": "{\"authorization\":{\"requests\":[{\"signer\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"network\":\"0u16\",\"program\":\"puzzle_spinner_v002.aleo\",\"function\":\"spin\",\"input_ids\":[{\"type\":\"external_record\",\"id\":\"2094094279846006863789449599365264356524704258957209392883419734916308087566field\"},{\"type\":\"public\",\"id\":\"5595943493906333085391638370632238393662235874383695353608521124649733340931field\"},{\"type\":\"private\",\"id\":\"6742098636398522089859394195904601600130314965171698086111626341696733948612field\"}],\"inputs\":[\"{\\n owner: aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a.private,\\n amount: 1000000u64.private,\\n _nonce: 3552842606932684888288059180163265370185468417773274452409126263862773749561group.public,\\n _version: 0u8.public\\n}\",\"{\\n nonce: 1170758118field,\\n tickets: 5000000u64\\n}\",\"sign1qkveh904rhh9q72mvg7r9p20q54w73w55m375dplgqmg4dj07cqgfu6nmyennfeyvczvdlsztndg3vstm5wrdx8gwl7ucjp8mmlwsqxyx67umlz8tz8pw8zk599sj05tsqczr4ufz06e4jl0lve0k6p3pvyztyddnpcpvq3p66k3ryatluay3cndws6fktfvnytg3hcswahqjdc7n92\"],\"signature\":\"sign1xf53lt5wp9g88h6s65uct4ps7te4p2a3fxt9lnj4mpuflltw9yqrxn4mslff0z7rehgher4s68pmcar9ul28c97kp2qsr8ar6ftfzpqmvqrdssu9887su5jga24rxwjhf9lt5lhk7zd2uqvc6w4stclkzxgnhkd24nmz0l050ejcf8e4tjugy8hrglglean3rne7c9907usqg287l48\",\"sk_tag\":\"2188950535000273989556007384792744144966780534890918446938267316970960124853field\",\"tvk\":\"572183841960682933577383606586553460249103306203159528948819230484782997585field\",\"tcm\":\"1171065817681420457160345788668531223791133417763996992400327530900798825721field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"signer\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"network\":\"0u16\",\"program\":\"puzzle_arcade_coin_v002.aleo\",\"function\":\"spend\",\"input_ids\":[{\"type\":\"record\",\"commitment\":\"3695326969420433429322937563366478414189257375833807166677296859078066945588field\",\"gamma\":\"822030278715035804872365307657119524241344315777773665392070818824848423298group\",\"record_view_key\":\"1925121045312432202191309331127274276680205187304403149300554031033906128262field\",\"serial_number\":\"4076970251659226920097956648171573791328895534043196534502480362996698993709field\",\"tag\":\"2912214195050022078917991120929452175415564426930328303911346972205167398771field\"},{\"type\":\"public\",\"id\":\"2041891405937960445696743302415641847824825632459410269684813295953846950757field\"}],\"inputs\":[\"{\\n owner: aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a.private,\\n amount: 1000000u64.private,\\n _nonce: 3552842606932684888288059180163265370185468417773274452409126263862773749561group.public,\\n _version: 0u8.public\\n}\",\"1000000u64\"],\"signature\":\"sign1yjce07tv55km8qsfcgg5f53yvx4yal4ruuf9u0eaxku6fq4wrgpg0l7hfgnvpvvalfl4qvces3k9s9nt9ugy6wwjs9mnu2exdxz3cqsmvqrdssu9887su5jga24rxwjhf9lt5lhk7zd2uqvc6w4stclkzxgnhkd24nmz0l050ejcf8e4tjugy8hrglglean3rne7c9907usqgm4ynq3\",\"sk_tag\":\"2188950535000273989556007384792744144966780534890918446938267316970960124853field\",\"tvk\":\"4994084056607172960767483219582395385778247231446264109964208478083811415449field\",\"tcm\":\"7812759430980949738683330164278254095249178692436543417615689108707612536911field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"signer\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"network\":\"0u16\",\"program\":\"puzzle_arcade_ticket_v002.aleo\",\"function\":\"mint\",\"input_ids\":[{\"type\":\"public\",\"id\":\"1046274269999797229672577130372021146162188188700658720275906690284075339674field\"},{\"type\":\"public\",\"id\":\"6672330946491079853759778561061110692770616941692333781924093458318671076471field\"}],\"inputs\":[\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"5000000u64\"],\"signature\":\"sign1ycfa5yhx8magvj47z7ucdaaj6dqaglhgdwzy6930uh4qx8vum5q9wx7arq707cedudzld6mca2gzdex90utr0eezl8y4479sc02a5qqmvqrdssu9887su5jga24rxwjhf9lt5lhk7zd2uqvc6w4stclkzxgnhkd24nmz0l050ejcf8e4tjugy8hrglglean3rne7c9907usqgx56dh0\",\"sk_tag\":\"2188950535000273989556007384792744144966780534890918446938267316970960124853field\",\"tvk\":\"2873294706176964472715353629140777692023106080534816614008391405046442842512field\",\"tcm\":\"6490794758489608132610463651546131626878006045467732980547897283109192374041field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"}],\"transitions\":[{\"id\":\"au1234a2r7zve0049hcsm3fawmk2frt5p6zvfn4v2algsmmemjk7uxst0z669\",\"program\":\"puzzle_arcade_coin_v002.aleo\",\"function\":\"spend\",\"inputs\":[{\"type\":\"record\",\"id\":\"4076970251659226920097956648171573791328895534043196534502480362996698993709field\",\"tag\":\"2912214195050022078917991120929452175415564426930328303911346972205167398771field\"},{\"type\":\"public\",\"id\":\"2041891405937960445696743302415641847824825632459410269684813295953846950757field\",\"value\":\"1000000u64\"}],\"outputs\":[{\"type\":\"record\",\"id\":\"6234733124236322387889736135649408851334996209733769544324065144060617746336field\",\"checksum\":\"440246515958659636861403206156566406152482039155384175770866473086825655202field\",\"value\":\"record1qvqspknecyxj8he7r7d0saa8mu8h4655jt25xgvfnryxg37td4hhzlg3qyrxzmt0w4h8ggcqqgqspm7g76ludumjncxeafx7hqch36k0q5cdvrxctm0w0h05jx9q0esqty29y8hfvl5sqercpezunplc89k36fxls209lyqqmvun5h30dcgq4newd4\",\"sender_ciphertext\":\"2051089804256883694774647491009107449590332829064597128043238087312538756881field\"}],\"tpk\":\"779450742859646977077288858452846617400181919260356985744863858124988996579group\",\"tcm\":\"7812759430980949738683330164278254095249178692436543417615689108707612536911field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"id\":\"au165pk3mj0z8qmd4rjrdpymex0wg2ktjg69jlh0ddgq3r8a8xw4y9qcerjde\",\"program\":\"puzzle_arcade_ticket_v002.aleo\",\"function\":\"mint\",\"inputs\":[{\"type\":\"public\",\"id\":\"1046274269999797229672577130372021146162188188700658720275906690284075339674field\",\"value\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\"},{\"type\":\"public\",\"id\":\"6672330946491079853759778561061110692770616941692333781924093458318671076471field\",\"value\":\"5000000u64\"}],\"outputs\":[{\"type\":\"record\",\"id\":\"1130818339127085714202916297407753311196018055225024066502861268664315562572field\",\"checksum\":\"6564309912145323765833525250549682602535323908901729171643580386651654016412field\",\"value\":\"record1qvqsp8svu763tlxwmh9wdmdu93fs4ydgmsk62kqz7h2egshvlfjcfcc9qyrxzmt0w4h8ggcqqgqsqlradgpmpxuephuktakm3r9k6849czlyw7nw5u8zhxfuwrhx3mszxrg96enap4k0fmjyhsdqpvx6nyvc8k6t8st3pvrvd4jghh836srsv8enll\",\"sender_ciphertext\":\"3633731045592752368422559477739611800333912219171005386498755402947798716823field\"},{\"type\":\"future\",\"id\":\"74131887599252140728099600235197072782607427140742230946502591809048135927field\",\"value\":\"{\\n program_id: puzzle_arcade_ticket_v002.aleo,\\n function_name: mint,\\n arguments: [\\n aleo1p8wg0ps5tzqt6hqehczvngszvgrl064d8jahz3axpkeza8preurqshnmpj\\n ]\\n}\"}],\"tpk\":\"2840319856609594596522635087311530377650934567024220337860469891161374663385group\",\"tcm\":\"6490794758489608132610463651546131626878006045467732980547897283109192374041field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"id\":\"au1jmm0qqwmgl06vr42ya43kcv0pjsgt7huyydtr0j9cvrsy3allqgsulx0d8\",\"program\":\"puzzle_spinner_v002.aleo\",\"function\":\"spin\",\"inputs\":[{\"type\":\"external_record\",\"id\":\"2094094279846006863789449599365264356524704258957209392883419734916308087566field\"},{\"type\":\"public\",\"id\":\"5595943493906333085391638370632238393662235874383695353608521124649733340931field\",\"value\":\"{\\n nonce: 1170758118field,\\n tickets: 5000000u64\\n}\"},{\"type\":\"private\",\"id\":\"6742098636398522089859394195904601600130314965171698086111626341696733948612field\",\"value\":\"ciphertext1q5qqkh55w4tat8z30xdwfpzs2rtu8nayf0vtaj8fxzar9d26wce0jrfzq05mm5zl54rjfstpwy9rmdy09k5tkjzc280xzspwrdnprql8qh966uctg8hxuh9pxse6dz25u9ljxyxz6tvgzpj8s8ky3uta7f5qd0lftt5ksmexyyqtzsd04fsqgf9k8723zl4wt2spx9rjr38wrhsy3n56ej9zn88gravxdk7fape7jr7pgdam4g8vuey47ywzdtkrpc8qrsnfv0\"}],\"outputs\":[{\"type\":\"external_record\",\"id\":\"2735321205447718152398768122882561932543144412730538395102830561769369606454field\"},{\"type\":\"external_record\",\"id\":\"7369296498063689826492338091106358165388792727887425815548124115560783555312field\"},{\"type\":\"future\",\"id\":\"6484631562279855668848888140360023825065176810215937222335315467885649991363field\",\"value\":\"{\\n program_id: puzzle_spinner_v002.aleo,\\n function_name: spin,\\n arguments: [\\n {\\n program_id: puzzle_arcade_ticket_v002.aleo,\\n function_name: mint,\\n arguments: [\\n aleo1p8wg0ps5tzqt6hqehczvngszvgrl064d8jahz3axpkeza8preurqshnmpj\\n ]\\n },\\n 1170758118field\\n ]\\n}\"}],\"tpk\":\"6590493951761094593568034471222493140361737341714336847530563463808843573584group\",\"tcm\":\"1171065817681420457160345788668531223791133417763996992400327530900798825721field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"}]},\"fee_authorization\":{\"requests\":[{\"signer\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"network\":\"0u16\",\"program\":\"credits.aleo\",\"function\":\"fee_public\",\"input_ids\":[{\"type\":\"public\",\"id\":\"1236904445320008365690109681543399166087562464648307672887712965547911005106field\"},{\"type\":\"public\",\"id\":\"7534378790668398972390701196841804750368436610916174309476130285208807336075field\"},{\"type\":\"public\",\"id\":\"3839097647761853616188014373153230802709413936709969178171401168479940342742field\"}],\"inputs\":[\"1000000u64\",\"0u64\",\"5861750103935316794608259263097700543376455430267921167932279423298832828187field\"],\"signature\":\"sign1em3pwu4xle9lcqr2qtj6fek75u5ntt8dxnmkcc80pjylc5hed5qjz488ga0fcx6quphlps95qlf839q77mhgv3dezf5dlzame5hrgqsmvqrdssu9887su5jga24rxwjhf9lt5lhk7zd2uqvc6w4stclkzxgnhkd24nmz0l050ejcf8e4tjugy8hrglglean3rne7c9907usqgdtex07\",\"sk_tag\":\"2188950535000273989556007384792744144966780534890918446938267316970960124853field\",\"tvk\":\"842468693271079620554301388750085740873203583062890643188623955488839009625field\",\"tcm\":\"2752533455412613106771920890566519746466954607696065566492577296743291985261field\",\"scm\":\"3844647255947359656650853949587322431249425751036754772276995840173728179480field\"}],\"transitions\":[{\"id\":\"au13uuftt5a73ha0ez9hyaqc7f3efkyw0nmufvfsa35ze3cuggwegyqfmpsnc\",\"program\":\"credits.aleo\",\"function\":\"fee_public\",\"inputs\":[{\"type\":\"public\",\"id\":\"1236904445320008365690109681543399166087562464648307672887712965547911005106field\",\"value\":\"1000000u64\"},{\"type\":\"public\",\"id\":\"7534378790668398972390701196841804750368436610916174309476130285208807336075field\",\"value\":\"0u64\"},{\"type\":\"public\",\"id\":\"3839097647761853616188014373153230802709413936709969178171401168479940342742field\",\"value\":\"5861750103935316794608259263097700543376455430267921167932279423298832828187field\"}],\"outputs\":[{\"type\":\"future\",\"id\":\"7376837628727201465092024949171324267883508382872525506751191952280391240088field\",\"value\":\"{\\n program_id: credits.aleo,\\n function_name: fee_public,\\n arguments: [\\n aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a,\\n 1000000u64\\n ]\\n}\"}],\"tpk\":\"4674317274796085762374780614101659015809429371081752559519443134812441140952group\",\"tcm\":\"2752533455412613106771920890566519746466954607696065566492577296743291985261field\",\"scm\":\"3844647255947359656650853949587322431249425751036754772276995840173728179480field\"}]},\"broadcast\":false}", + "PUZZLE_SPINNER_V002_AUTHORIZATION": "{\"requests\":[{\"signer\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"network\":\"0u16\",\"program\":\"puzzle_spinner_v002.aleo\",\"function\":\"spin\",\"input_ids\":[{\"type\":\"external_record\",\"id\":\"2094094279846006863789449599365264356524704258957209392883419734916308087566field\"},{\"type\":\"public\",\"id\":\"5595943493906333085391638370632238393662235874383695353608521124649733340931field\"},{\"type\":\"private\",\"id\":\"6742098636398522089859394195904601600130314965171698086111626341696733948612field\"}],\"inputs\":[\"{\\n owner: aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a.private,\\n amount: 1000000u64.private,\\n _nonce: 3552842606932684888288059180163265370185468417773274452409126263862773749561group.public,\\n _version: 0u8.public\\n}\",\"{\\n nonce: 1170758118field,\\n tickets: 5000000u64\\n}\",\"sign1qkveh904rhh9q72mvg7r9p20q54w73w55m375dplgqmg4dj07cqgfu6nmyennfeyvczvdlsztndg3vstm5wrdx8gwl7ucjp8mmlwsqxyx67umlz8tz8pw8zk599sj05tsqczr4ufz06e4jl0lve0k6p3pvyztyddnpcpvq3p66k3ryatluay3cndws6fktfvnytg3hcswahqjdc7n92\"],\"signature\":\"sign1xf53lt5wp9g88h6s65uct4ps7te4p2a3fxt9lnj4mpuflltw9yqrxn4mslff0z7rehgher4s68pmcar9ul28c97kp2qsr8ar6ftfzpqmvqrdssu9887su5jga24rxwjhf9lt5lhk7zd2uqvc6w4stclkzxgnhkd24nmz0l050ejcf8e4tjugy8hrglglean3rne7c9907usqg287l48\",\"sk_tag\":\"2188950535000273989556007384792744144966780534890918446938267316970960124853field\",\"tvk\":\"572183841960682933577383606586553460249103306203159528948819230484782997585field\",\"tcm\":\"1171065817681420457160345788668531223791133417763996992400327530900798825721field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"signer\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"network\":\"0u16\",\"program\":\"puzzle_arcade_coin_v002.aleo\",\"function\":\"spend\",\"input_ids\":[{\"type\":\"record\",\"commitment\":\"3695326969420433429322937563366478414189257375833807166677296859078066945588field\",\"gamma\":\"822030278715035804872365307657119524241344315777773665392070818824848423298group\",\"record_view_key\":\"1925121045312432202191309331127274276680205187304403149300554031033906128262field\",\"serial_number\":\"4076970251659226920097956648171573791328895534043196534502480362996698993709field\",\"tag\":\"2912214195050022078917991120929452175415564426930328303911346972205167398771field\"},{\"type\":\"public\",\"id\":\"2041891405937960445696743302415641847824825632459410269684813295953846950757field\"}],\"inputs\":[\"{\\n owner: aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a.private,\\n amount: 1000000u64.private,\\n _nonce: 3552842606932684888288059180163265370185468417773274452409126263862773749561group.public,\\n _version: 0u8.public\\n}\",\"1000000u64\"],\"signature\":\"sign1yjce07tv55km8qsfcgg5f53yvx4yal4ruuf9u0eaxku6fq4wrgpg0l7hfgnvpvvalfl4qvces3k9s9nt9ugy6wwjs9mnu2exdxz3cqsmvqrdssu9887su5jga24rxwjhf9lt5lhk7zd2uqvc6w4stclkzxgnhkd24nmz0l050ejcf8e4tjugy8hrglglean3rne7c9907usqgm4ynq3\",\"sk_tag\":\"2188950535000273989556007384792744144966780534890918446938267316970960124853field\",\"tvk\":\"4994084056607172960767483219582395385778247231446264109964208478083811415449field\",\"tcm\":\"7812759430980949738683330164278254095249178692436543417615689108707612536911field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"signer\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"network\":\"0u16\",\"program\":\"puzzle_arcade_ticket_v002.aleo\",\"function\":\"mint\",\"input_ids\":[{\"type\":\"public\",\"id\":\"1046274269999797229672577130372021146162188188700658720275906690284075339674field\"},{\"type\":\"public\",\"id\":\"6672330946491079853759778561061110692770616941692333781924093458318671076471field\"}],\"inputs\":[\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\",\"5000000u64\"],\"signature\":\"sign1ycfa5yhx8magvj47z7ucdaaj6dqaglhgdwzy6930uh4qx8vum5q9wx7arq707cedudzld6mca2gzdex90utr0eezl8y4479sc02a5qqmvqrdssu9887su5jga24rxwjhf9lt5lhk7zd2uqvc6w4stclkzxgnhkd24nmz0l050ejcf8e4tjugy8hrglglean3rne7c9907usqgx56dh0\",\"sk_tag\":\"2188950535000273989556007384792744144966780534890918446938267316970960124853field\",\"tvk\":\"2873294706176964472715353629140777692023106080534816614008391405046442842512field\",\"tcm\":\"6490794758489608132610463651546131626878006045467732980547897283109192374041field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"}],\"transitions\":[{\"id\":\"au1234a2r7zve0049hcsm3fawmk2frt5p6zvfn4v2algsmmemjk7uxst0z669\",\"program\":\"puzzle_arcade_coin_v002.aleo\",\"function\":\"spend\",\"inputs\":[{\"type\":\"record\",\"id\":\"4076970251659226920097956648171573791328895534043196534502480362996698993709field\",\"tag\":\"2912214195050022078917991120929452175415564426930328303911346972205167398771field\"},{\"type\":\"public\",\"id\":\"2041891405937960445696743302415641847824825632459410269684813295953846950757field\",\"value\":\"1000000u64\"}],\"outputs\":[{\"type\":\"record\",\"id\":\"6234733124236322387889736135649408851334996209733769544324065144060617746336field\",\"checksum\":\"440246515958659636861403206156566406152482039155384175770866473086825655202field\",\"value\":\"record1qvqspknecyxj8he7r7d0saa8mu8h4655jt25xgvfnryxg37td4hhzlg3qyrxzmt0w4h8ggcqqgqspm7g76ludumjncxeafx7hqch36k0q5cdvrxctm0w0h05jx9q0esqty29y8hfvl5sqercpezunplc89k36fxls209lyqqmvun5h30dcgq4newd4\",\"sender_ciphertext\":\"2051089804256883694774647491009107449590332829064597128043238087312538756881field\"}],\"tpk\":\"779450742859646977077288858452846617400181919260356985744863858124988996579group\",\"tcm\":\"7812759430980949738683330164278254095249178692436543417615689108707612536911field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"id\":\"au165pk3mj0z8qmd4rjrdpymex0wg2ktjg69jlh0ddgq3r8a8xw4y9qcerjde\",\"program\":\"puzzle_arcade_ticket_v002.aleo\",\"function\":\"mint\",\"inputs\":[{\"type\":\"public\",\"id\":\"1046274269999797229672577130372021146162188188700658720275906690284075339674field\",\"value\":\"aleo12a4wll9ax6w5355jph0dr5wt2vla5sss2t4cnch0tc3vzh643v8qcfvc7a\"},{\"type\":\"public\",\"id\":\"6672330946491079853759778561061110692770616941692333781924093458318671076471field\",\"value\":\"5000000u64\"}],\"outputs\":[{\"type\":\"record\",\"id\":\"1130818339127085714202916297407753311196018055225024066502861268664315562572field\",\"checksum\":\"6564309912145323765833525250549682602535323908901729171643580386651654016412field\",\"value\":\"record1qvqsp8svu763tlxwmh9wdmdu93fs4ydgmsk62kqz7h2egshvlfjcfcc9qyrxzmt0w4h8ggcqqgqsqlradgpmpxuephuktakm3r9k6849czlyw7nw5u8zhxfuwrhx3mszxrg96enap4k0fmjyhsdqpvx6nyvc8k6t8st3pvrvd4jghh836srsv8enll\",\"sender_ciphertext\":\"3633731045592752368422559477739611800333912219171005386498755402947798716823field\"},{\"type\":\"future\",\"id\":\"74131887599252140728099600235197072782607427140742230946502591809048135927field\",\"value\":\"{\\n program_id: puzzle_arcade_ticket_v002.aleo,\\n function_name: mint,\\n arguments: [\\n aleo1p8wg0ps5tzqt6hqehczvngszvgrl064d8jahz3axpkeza8preurqshnmpj\\n ]\\n}\"}],\"tpk\":\"2840319856609594596522635087311530377650934567024220337860469891161374663385group\",\"tcm\":\"6490794758489608132610463651546131626878006045467732980547897283109192374041field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"},{\"id\":\"au1jmm0qqwmgl06vr42ya43kcv0pjsgt7huyydtr0j9cvrsy3allqgsulx0d8\",\"program\":\"puzzle_spinner_v002.aleo\",\"function\":\"spin\",\"inputs\":[{\"type\":\"external_record\",\"id\":\"2094094279846006863789449599365264356524704258957209392883419734916308087566field\"},{\"type\":\"public\",\"id\":\"5595943493906333085391638370632238393662235874383695353608521124649733340931field\",\"value\":\"{\\n nonce: 1170758118field,\\n tickets: 5000000u64\\n}\"},{\"type\":\"private\",\"id\":\"6742098636398522089859394195904601600130314965171698086111626341696733948612field\",\"value\":\"ciphertext1q5qqkh55w4tat8z30xdwfpzs2rtu8nayf0vtaj8fxzar9d26wce0jrfzq05mm5zl54rjfstpwy9rmdy09k5tkjzc280xzspwrdnprql8qh966uctg8hxuh9pxse6dz25u9ljxyxz6tvgzpj8s8ky3uta7f5qd0lftt5ksmexyyqtzsd04fsqgf9k8723zl4wt2spx9rjr38wrhsy3n56ej9zn88gravxdk7fape7jr7pgdam4g8vuey47ywzdtkrpc8qrsnfv0\"}],\"outputs\":[{\"type\":\"external_record\",\"id\":\"2735321205447718152398768122882561932543144412730538395102830561769369606454field\"},{\"type\":\"external_record\",\"id\":\"7369296498063689826492338091106358165388792727887425815548124115560783555312field\"},{\"type\":\"future\",\"id\":\"6484631562279855668848888140360023825065176810215937222335315467885649991363field\",\"value\":\"{\\n program_id: puzzle_spinner_v002.aleo,\\n function_name: spin,\\n arguments: [\\n {\\n program_id: puzzle_arcade_ticket_v002.aleo,\\n function_name: mint,\\n arguments: [\\n aleo1p8wg0ps5tzqt6hqehczvngszvgrl064d8jahz3axpkeza8preurqshnmpj\\n ]\\n },\\n 1170758118field\\n ]\\n}\"}],\"tpk\":\"6590493951761094593568034471222493140361737341714336847530563463808843573584group\",\"tcm\":\"1171065817681420457160345788668531223791133417763996992400327530900798825721field\",\"scm\":\"626039704492388670135868348830115832773688775904731209381377014140356519273field\"}]}" +} \ No newline at end of file diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 8279815..60f1736 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -52,6 +52,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Credits>()?; m.add_class::<DynamicRecord>()?; m.add_class::<Execution>()?; + m.add_class::<ExecutionRequest>()?; m.add_class::<Fee>()?; m.add_class::<Field>()?; m.add_class::<GraphKey>()?; @@ -81,6 +82,7 @@ fn register(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Program>()?; m.add_class::<ProgramID>()?; m.add_class::<ProvingKey>()?; + m.add_class::<ProvingRequest>()?; m.add_class::<Query>()?; m.add_class::<RecordCiphertext>()?; m.add_class::<RecordPlaintext>()?; diff --git a/sdk/src/programs/execution_request.rs b/sdk/src/programs/execution_request.rs new file mode 100644 index 0000000..394cb79 --- /dev/null +++ b/sdk/src/programs/execution_request.rs @@ -0,0 +1,216 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + types::{ + FieldNative, IdentifierNative, ProgramIDNative, RequestNative, ValueNative, ValueTypeNative, + }, + Address, Field, PrivateKey, Signature, +}; + +use pyo3::prelude::*; +use rand::rngs::StdRng; +use snarkvm::prelude::{FromBytes, ToBytes}; + +use std::{ops::Deref, str::FromStr}; + +/// A single signed snarkVM `Request` for a program function call. +/// +/// Used by the delegated proving service: a `Request` is signed client-side and +/// the server turns it into an `Authorization` via `Process::authorize_request` +/// before proving. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct ExecutionRequest(RequestNative); + +#[pymethods] +impl ExecutionRequest { + /// Creates a new request by signing over a program ID and set of inputs. + /// + /// `root_tvk` is the tvk of the function at the top of the call graph; + /// `None` for a top-level call or a single-function call graph. + /// `program_checksum` is the checksum of the program; `None` if the call is + /// not dynamic. `is_root` indicates the top level function in the call graph + /// and `is_dynamic` indicates a dynamic call. + #[staticmethod] + #[pyo3(signature = (private_key, program_id, function_name, inputs, input_types, root_tvk, program_checksum, is_root, is_dynamic))] + #[allow(clippy::too_many_arguments)] + fn sign( + private_key: &PrivateKey, + program_id: &str, + function_name: &str, + inputs: Vec<String>, + input_types: Vec<String>, + root_tvk: Option<Field>, + program_checksum: Option<Field>, + is_root: bool, + is_dynamic: bool, + ) -> anyhow::Result<Self> { + // Convert the ProgramID and function name to their native objects. + let program_id = ProgramIDNative::from_str(program_id)?; + let function_name = IdentifierNative::from_str(function_name)?; + + // Ensure the inputs are valid Aleo types. + let inputs = inputs + .iter() + .map(|input| ValueNative::from_str(input)) + .collect::<anyhow::Result<Vec<ValueNative>>>()?; + + // Ensure the input types are valid Aleo value types. + let input_types = input_types + .iter() + .map(|input_type| ValueTypeNative::from_str(input_type)) + .collect::<anyhow::Result<Vec<ValueTypeNative>>>()?; + + // Get the root tvk and program checksum if specified. + let root_tvk = root_tvk.map(FieldNative::from); + let program_checksum = program_checksum.map(FieldNative::from); + + // Generate an RNG for the function from system entropy. + let mut rng = rand::make_rng::<StdRng>(); + + // Generate the signature over the request. + let request = RequestNative::sign( + private_key, + program_id, + function_name, + inputs.into_iter(), + &input_types, + root_tvk, + is_root, + program_checksum, + is_dynamic, + &mut rng, + )?; + + Ok(ExecutionRequest(request)) + } + + /// Builds a request object from a string representation of a request. + #[staticmethod] + fn from_string(request: &str) -> anyhow::Result<Self> { + Ok(ExecutionRequest(RequestNative::from_str(request)?)) + } + + /// Constructs a request from a byte array. + #[staticmethod] + fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> { + RequestNative::from_bytes_le(bytes).map(ExecutionRequest) + } + + /// Returns the byte representation of the request. + fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Returns the request signer. + fn signer(&self) -> Address { + Address::from(*self.0.signer()) + } + + /// Returns the network ID. + fn network_id(&self) -> u16 { + **self.0.network_id() + } + + /// Returns the program ID. + fn program_id(&self) -> String { + self.0.program_id().to_string() + } + + /// Returns the function name. + fn function_name(&self) -> String { + self.0.function_name().to_string() + } + + /// Returns the function inputs as a list of strings. + fn inputs(&self) -> Vec<String> { + self.0 + .inputs() + .iter() + .map(|input| input.to_string()) + .collect() + } + + /// Returns the signature for the transition. + fn signature(&self) -> Signature { + Signature::from(*self.0.signature()) + } + + /// Returns the tag secret key `sk_tag`. + fn sk_tag(&self) -> Field { + Field::from(*self.0.sk_tag()) + } + + /// Returns the transition view key `tvk`. + fn tvk(&self) -> Field { + Field::from(*self.0.tvk()) + } + + /// Returns the transition commitment `tcm`. + fn tcm(&self) -> Field { + Field::from(*self.0.tcm()) + } + + /// Returns the signer commitment `scm`. + fn scm(&self) -> Field { + Field::from(*self.0.scm()) + } + + /// Returns the request as a JSON string. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } + + #[classattr] + const __hash__: Option<PyObject> = None; +} + +impl Deref for ExecutionRequest { + type Target = RequestNative; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<RequestNative> for ExecutionRequest { + fn from(value: RequestNative) -> Self { + Self(value) + } +} + +impl From<&RequestNative> for ExecutionRequest { + fn from(value: &RequestNative) -> Self { + Self(value.clone()) + } +} + +impl From<ExecutionRequest> for RequestNative { + fn from(value: ExecutionRequest) -> Self { + value.0 + } +} + +impl From<&ExecutionRequest> for RequestNative { + fn from(value: &ExecutionRequest) -> Self { + value.0.clone() + } +} diff --git a/sdk/src/programs/mod.rs b/sdk/src/programs/mod.rs index fd4c1ab..1236625 100644 --- a/sdk/src/programs/mod.rs +++ b/sdk/src/programs/mod.rs @@ -23,6 +23,9 @@ pub use dynamic_record::DynamicRecord; mod execution; pub use execution::Execution; +mod execution_request; +pub use execution_request::ExecutionRequest; + mod fee; pub use fee::Fee; @@ -53,6 +56,9 @@ pub use program::Program; mod proving_key; pub use proving_key::ProvingKey; +mod proving_request; +pub use proving_request::ProvingRequest; + mod response; pub use response::Response; diff --git a/sdk/src/programs/proving_request.rs b/sdk/src/programs/proving_request.rs new file mode 100644 index 0000000..0211852 --- /dev/null +++ b/sdk/src/programs/proving_request.rs @@ -0,0 +1,390 @@ +// Copyright (C) 2019-2023 Aleo Systems 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 <https://www.gnu.org/licenses/>. + +use crate::{ + types::{AuthorizationNative, RequestNative}, + Authorization, ExecutionRequest, +}; + +use pyo3::prelude::*; +use snarkvm::prelude::{FromBytes, ToBytes}; + +use std::{ + io, + io::{Read, Write}, + str::FromStr, +}; + +/// A proving request submitted to the Delegated Proving Service. +/// +/// Mirrors the variant layout used by the DPS: `Authorization` carries a +/// fully-constructed `Authorization` (and optional fee `Authorization`); +/// `Request` carries a single signed `Request` (and optional fee `Request`) +/// that the server turns into an `Authorization` via +/// `Process::authorize_request`. JSON is serialized untagged - the field shape +/// (`authorization`/`fee_authorization` vs. `request`/`fee_request`) determines +/// the variant. Bytes carry no discriminator; the reader must know the variant +/// out-of-band. +#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +// The variants intentionally hold the native types inline (no boxing) to mirror +// the wasm reference and keep the byte layout in `write_le`/`read_*_le` exact. +#[allow(clippy::large_enum_variant)] +pub enum ProvingRequestInner { + Authorization { + authorization: AuthorizationNative, + fee_authorization: Option<AuthorizationNative>, + broadcast: bool, + }, + Request { + request: RequestNative, + fee_request: Option<RequestNative>, + broadcast: bool, + }, +} + +impl ProvingRequestInner { + /// Returns the Authorization variant's inner authorization, or `None` if + /// this is a Request variant. + fn authorization(&self) -> Option<&AuthorizationNative> { + match self { + Self::Authorization { authorization, .. } => Some(authorization), + Self::Request { .. } => None, + } + } + + /// Returns the Authorization variant's fee authorization, or `None` if this + /// is a Request variant or no fee authorization is set. + fn fee_authorization(&self) -> Option<&AuthorizationNative> { + match self { + Self::Authorization { + fee_authorization, .. + } => fee_authorization.as_ref(), + Self::Request { .. } => None, + } + } + + /// Returns the Request variant's inner request, or `None` if this is an + /// Authorization variant. + fn request(&self) -> Option<&RequestNative> { + match self { + Self::Request { request, .. } => Some(request), + Self::Authorization { .. } => None, + } + } + + /// Returns the Request variant's fee request, or `None` if this is an + /// Authorization variant or no fee request is set. + fn fee_request(&self) -> Option<&RequestNative> { + match self { + Self::Request { fee_request, .. } => fee_request.as_ref(), + Self::Authorization { .. } => None, + } + } + + /// Returns the broadcast flag regardless of variant. + fn broadcast(&self) -> bool { + match self { + Self::Authorization { broadcast, .. } | Self::Request { broadcast, .. } => *broadcast, + } + } + + fn is_authorization(&self) -> bool { + matches!(self, Self::Authorization { .. }) + } + + fn is_request(&self) -> bool { + matches!(self, Self::Request { .. }) + } + + /// Reads bytes as the Authorization variant. + /// Layout: `authorization | bool | maybe(fee_authorization) | bool(broadcast)`. + fn read_authorization_le<R: Read>(mut reader: R) -> io::Result<Self> { + let authorization = AuthorizationNative::read_le(&mut reader)?; + let has_fee_auth = bool::read_le(&mut reader)?; + let fee_authorization = match has_fee_auth { + false => None, + true => Some(AuthorizationNative::read_le(&mut reader)?), + }; + let broadcast = bool::read_le(&mut reader)?; + Ok(Self::Authorization { + authorization, + fee_authorization, + broadcast, + }) + } + + /// Reads bytes as the Request variant. + /// Layout: `request | bool | maybe(fee_request) | bool(broadcast)`. + fn read_request_le<R: Read>(mut reader: R) -> io::Result<Self> { + let request = RequestNative::read_le(&mut reader)?; + let has_fee_request = bool::read_le(&mut reader)?; + let fee_request = match has_fee_request { + false => None, + true => Some(RequestNative::read_le(&mut reader)?), + }; + let broadcast = bool::read_le(&mut reader)?; + Ok(Self::Request { + request, + fee_request, + broadcast, + }) + } +} + +impl ToBytes for ProvingRequestInner { + /// Variant-aware byte serialization matching the DPS layout exactly. + /// No discriminator byte is written - the reader must know the variant + /// out-of-band. + fn write_le<W: Write>(&self, mut writer: W) -> io::Result<()> { + match self { + Self::Authorization { + authorization, + fee_authorization, + broadcast, + } => { + authorization.write_le(&mut writer)?; + match fee_authorization { + Some(auth) => { + true.write_le(&mut writer)?; + auth.write_le(&mut writer)?; + } + None => { + false.write_le(&mut writer)?; + } + } + broadcast.write_le(&mut writer)?; + } + Self::Request { + request, + fee_request, + broadcast, + } => { + request.write_le(&mut writer)?; + match fee_request { + Some(req) => { + true.write_le(&mut writer)?; + req.write_le(&mut writer)?; + } + None => { + false.write_le(&mut writer)?; + } + } + broadcast.write_le(&mut writer)?; + } + } + Ok(()) + } +} + +impl FromBytes for ProvingRequestInner { + /// Reads bytes as the Authorization variant for back-compat. To read the + /// Request variant, use [`ProvingRequestInner::read_request_le`] explicitly + /// because the byte layout carries no discriminator. + fn read_le<R: Read>(reader: R) -> io::Result<Self> { + Self::read_authorization_le(reader) + } +} + +impl FromStr for ProvingRequestInner { + type Err = anyhow::Error; + + fn from_str(request: &str) -> anyhow::Result<Self> { + Ok(serde_json::from_str(request)?) + } +} + +impl std::fmt::Display for ProvingRequestInner { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + serde_json::to_string(self).map_err(|_| std::fmt::Error)? + ) + } +} + +/// Represents a proving request to a prover. +/// +/// Carries one of two variants: +/// - `Authorization` - a fully-constructed `Authorization` (plus optional fee +/// authorization). +/// - `Request` - a single signed `Request` (plus optional fee `Request`) that +/// the prover authorizes server-side. +/// +/// Use `kind()` when handling a `ProvingRequest` of unknown variant (e.g. after +/// deserialization). Variant-specific accessors raise if called on the wrong +/// variant. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct ProvingRequest(ProvingRequestInner); + +#[pymethods] +impl ProvingRequest { + /// Creates a new Authorization-variant `ProvingRequest` from a function + /// `Authorization` and an optional fee `Authorization`. + #[new] + #[pyo3(signature = (authorization, fee_authorization, broadcast))] + fn new( + authorization: Authorization, + fee_authorization: Option<Authorization>, + broadcast: bool, + ) -> Self { + ProvingRequest(ProvingRequestInner::Authorization { + authorization: authorization.into(), + fee_authorization: fee_authorization.map(Into::into), + broadcast, + }) + } + + /// Creates a new Request-variant `ProvingRequest` from a single signed + /// `ExecutionRequest` and an optional signed fee `ExecutionRequest`. + #[staticmethod] + #[pyo3(signature = (request, fee_request, broadcast))] + fn from_request( + request: ExecutionRequest, + fee_request: Option<ExecutionRequest>, + broadcast: bool, + ) -> Self { + ProvingRequest(ProvingRequestInner::Request { + request: request.into(), + fee_request: fee_request.map(Into::into), + broadcast, + }) + } + + /// Returns the variant of this `ProvingRequest`: `"authorization"` or + /// `"request"`. + fn kind(&self) -> String { + if self.0.is_request() { + "request".to_string() + } else { + "authorization".to_string() + } + } + + /// Returns `True` if this is the Authorization variant. + fn is_authorization(&self) -> bool { + self.0.is_authorization() + } + + /// Returns `True` if this is the Request variant. + fn is_request(&self) -> bool { + self.0.is_request() + } + + /// Creates a `ProvingRequest` from a JSON string representation. The variant + /// is determined automatically by the JSON shape. + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + Ok(ProvingRequest(ProvingRequestInner::from_str(s)?)) + } + + /// Reads bytes as an Authorization-variant `ProvingRequest`. For the Request + /// variant, use `from_bytes_request` explicitly - byte layout carries no + /// variant discriminator. + #[staticmethod] + fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> { + ProvingRequestInner::read_authorization_le(bytes) + .map(ProvingRequest) + .map_err(Into::into) + } + + /// Reads bytes as a Request-variant `ProvingRequest`. Byte layout is + /// disjoint from the Authorization variant; callers must pick the right + /// reader for the bytes they hold. + #[staticmethod] + fn from_bytes_request(bytes: &[u8]) -> anyhow::Result<Self> { + ProvingRequestInner::read_request_le(bytes) + .map(ProvingRequest) + .map_err(Into::into) + } + + /// Creates a left-endian byte representation of the `ProvingRequest`, + /// dispatching on the variant. + fn bytes(&self) -> anyhow::Result<Vec<u8>> { + self.0.to_bytes_le() + } + + /// Returns the Authorization of the main function in the `ProvingRequest`. + /// + /// Raises if this `ProvingRequest` is a Request variant. + fn authorization(&self) -> anyhow::Result<Authorization> { + self.0 + .authorization() + .map(|a| Authorization::from(a.clone())) + .ok_or_else(|| { + anyhow::anyhow!( + "ProvingRequest is a Request variant; call `request()` instead of `authorization()`" + ) + }) + } + + /// Returns the fee Authorization in the `ProvingRequest`, or `None` when no + /// fee is set or this is a Request variant. + fn fee_authorization(&self) -> Option<Authorization> { + self.0 + .fee_authorization() + .map(|a| Authorization::from(a.clone())) + } + + /// Returns the signed `ExecutionRequest` carried by the Request variant. + /// + /// Raises if this `ProvingRequest` is an Authorization variant. + fn request(&self) -> anyhow::Result<ExecutionRequest> { + self.0.request().map(ExecutionRequest::from).ok_or_else(|| { + anyhow::anyhow!( + "ProvingRequest is an Authorization variant; call `authorization()` instead of `request()`" + ) + }) + } + + /// Returns the signed fee `ExecutionRequest` in the Request variant, or + /// `None` when no fee request is set or this is an Authorization variant. + fn fee_request(&self) -> Option<ExecutionRequest> { + self.0.fee_request().map(ExecutionRequest::from) + } + + /// Returns the broadcast flag set in the `ProvingRequest`. + #[getter] + fn broadcast(&self) -> bool { + self.0.broadcast() + } + + /// Returns the `ProvingRequest` as a JSON string. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } + + #[classattr] + const __hash__: Option<PyObject> = None; +} + +impl From<ProvingRequestInner> for ProvingRequest { + fn from(value: ProvingRequestInner) -> Self { + Self(value) + } +} + +impl From<ProvingRequest> for ProvingRequestInner { + fn from(value: ProvingRequest) -> Self { + value.0 + } +} diff --git a/sdk/src/types.rs b/sdk/src/types.rs index f05afb7..a0e952f 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -29,8 +29,8 @@ use snarkvm::ledger::store::helpers::memory::BlockMemory; use snarkvm::prelude::{ Address, Argument, Authorization, Boolean, Ciphertext, ComputeKey, Execution, Field, Future, GraphKey, Group, Identifier, Literal, Locator, Plaintext, PrivateKey, Program, ProgramID, - ProvingKey, Record, Response, Scalar, Signature, Transition, Value, VerifyingKey, ViewKey, - I128, I16, I32, I64, I8, U128, U16, U32, U64, U8, + ProvingKey, Record, Request, Response, Scalar, Signature, Transition, Value, ValueType, + VerifyingKey, ViewKey, I128, I16, I32, I64, I8, U128, U16, U32, U64, U8, }; use snarkvm::synthesizer::{Process, Trace}; @@ -83,6 +83,8 @@ pub type ProgramIDNative = ProgramID<CurrentNetwork>; pub type ProgramNative = Program<CurrentNetwork>; pub type ProvingKeyNative = ProvingKey<CurrentNetwork>; pub type QueryNative = Query<CurrentNetwork, CurrentBlockMemory>; +pub type RequestNative = Request<CurrentNetwork>; +pub type ValueTypeNative = ValueType<CurrentNetwork>; pub type ResponseNative = Response<CurrentNetwork>; pub type TraceNative = Trace<CurrentNetwork>; pub type TransactionNative = Transaction<CurrentNetwork>; From e4200ed85d3dff11f666f19c17927dca3ac170dd Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 13:55:41 -0400 Subject: [PATCH 60/72] feat: AleoNetworkClient (sync+async) with delegated proving submission Pure-Python AleoNetworkClient (requests) and AsyncAleoNetworkClient (httpx) mirroring TS SDK semantics: block/program/transaction endpoints, JWT refresh, DPS sealed-box encryption via PyNaCl, retry on 5xx, wait_for_confirmation. Shared logic in _client_common.py; security helpers in security.py. 519 tests pass (+62); pyright 0 errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .superpowers/sdd/task-w6-report.md | 31 + sdk/pyproject.toml | 7 + sdk/python/aleo/__init__.py | 4 + sdk/python/aleo/_client_common.py | 134 ++++ sdk/python/aleo/async_network_client.py | 455 ++++++++++++ sdk/python/aleo/network_client.py | 532 ++++++++++++++ sdk/python/aleo/security.py | 24 + sdk/python/tests/test_network_client.py | 671 ++++++++++++++++++ sdk/python/tests/test_network_client_async.py | 380 ++++++++++ sdk/python/tests/test_security.py | 95 +++ 10 files changed, 2333 insertions(+) create mode 100644 .superpowers/sdd/task-w6-report.md create mode 100644 sdk/python/aleo/_client_common.py create mode 100644 sdk/python/aleo/async_network_client.py create mode 100644 sdk/python/aleo/network_client.py create mode 100644 sdk/python/aleo/security.py create mode 100644 sdk/python/tests/test_network_client.py create mode 100644 sdk/python/tests/test_network_client_async.py create mode 100644 sdk/python/tests/test_security.py diff --git a/.superpowers/sdd/task-w6-report.md b/.superpowers/sdd/task-w6-report.md new file mode 100644 index 0000000..44bf07d --- /dev/null +++ b/.superpowers/sdd/task-w6-report.md @@ -0,0 +1,31 @@ +# W6: AleoNetworkClient — Task Report + +## Status: COMPLETE + +## Worktree +`/Users/privacydaddy/dev/python-sdk/.claude/worktrees/agent-a0edcc75d7a9560ad` + +## Test Counts +- Before: 457 passed (base at commit 4dbd621) +- After: 519 passed (+62 new tests) +- Pyright: 0 errors, 0 warnings + +## Files Added +- `sdk/python/aleo/_client_common.py` — shared models, errors, retry, JWT helpers +- `sdk/python/aleo/security.py` — PyNaCl sealed-box encrypt helper +- `sdk/python/aleo/network_client.py` — sync AleoNetworkClient (requests) +- `sdk/python/aleo/async_network_client.py` — async AsyncAleoNetworkClient (httpx) +- `sdk/python/tests/test_network_client.py` — 42 sync tests (mocked) +- `sdk/python/tests/test_network_client_async.py` — 20 async tests (mocked) +- `sdk/python/tests/test_security.py` — 5 sealed-box tests + +## Files Modified +- `sdk/python/aleo/__init__.py` — exports AleoNetworkClient, AsyncAleoNetworkClient, AleoNetworkError, AleoProvingError +- `sdk/pyproject.toml` — added requests dep, async/dps optional extras + +## Deviations from TS Semantics +- `find_records` / `find_unspent_records` omitted per scope change (deprecated in TS, superseded by delegated RecordScanner). +- Python WASM API differs from TS: `Program.from_source()` (not `fromString()`), `Program.imports` list attribute (not `getImports()`), `Transaction.from_json()` (not `fromString()`). +- `get_program_imports` uses a DFS helper `_collect_program_imports` to avoid double-fetching during recursion (same semantics as TS, different implementation to match Python WASM API). +- Helper functions use non-underscore names (`jwt_origin`, `make_default_headers`, etc.) to satisfy pyright's `reportPrivateUsage` rule in strict mode. +- Async retry patches `asyncio.sleep` at module level (not `aleo._client_common.asyncio.sleep`) because asyncio is imported inside the function body. diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 97cc1ca..0dcf066 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -13,6 +13,13 @@ classifiers = [ # Optional "Programming Language :: Python :: 3", ] +[project.dependencies] +dependencies = ["requests>=2"] + +[project.optional-dependencies] +async = ["httpx>=0.27"] +dps = ["pynacl>=1.5"] + [project.urls] "Homepage" = "https://github.com/AleoHQ/python-sdk" diff --git a/sdk/python/aleo/__init__.py b/sdk/python/aleo/__init__.py index e12a97d..c2053d1 100644 --- a/sdk/python/aleo/__init__.py +++ b/sdk/python/aleo/__init__.py @@ -2,3 +2,7 @@ from . import mainnet as mainnet from .encryptor import * +from .network_client import AleoNetworkClient as AleoNetworkClient +from .async_network_client import AsyncAleoNetworkClient as AsyncAleoNetworkClient +from ._client_common import AleoNetworkError as AleoNetworkError +from ._client_common import AleoProvingError as AleoProvingError diff --git a/sdk/python/aleo/_client_common.py b/sdk/python/aleo/_client_common.py new file mode 100644 index 0000000..70c7b83 --- /dev/null +++ b/sdk/python/aleo/_client_common.py @@ -0,0 +1,134 @@ +"""Shared models, error types, retry logic and JWT helpers for AleoNetworkClient.""" +from __future__ import annotations + +import random +import time +from typing import Any +from urllib.parse import urlparse + + +class AleoNetworkError(Exception): + """Raised when an Aleo network API call fails.""" + + def __init__(self, message: str, status: int | None = None) -> None: + super().__init__(message) + self.status = status + + +class AleoProvingError(Exception): + """Raised when DPS proving fails (non-retried errors).""" + + def __init__(self, message: str, status: int | None = None) -> None: + super().__init__(message) + self.status = status + + +FIVE_MINUTES_MS: int = 5 * 60 * 1000 +DEFAULT_HOST: str = "https://api.provable.com/v2" +DEFAULT_NETWORK: str = "mainnet" +SDK_HEADERS: set[str] = {"x-aleo-sdk-version", "x-aleo-environment", "x-aleo-method"} + + +def package_version() -> str: + try: + from importlib.metadata import version + return version("aleo") + except Exception: + return "0.0.0" + + +def make_default_headers() -> dict[str, str]: + return { + "X-Aleo-SDK-Version": package_version(), + "X-Aleo-environment": "python", + } + + +def user_headers(headers: dict[str, str]) -> dict[str, str]: + """Return only non-SDK headers (for custom transport mode).""" + return {k: v for k, v in headers.items() if k.lower() not in SDK_HEADERS} + + +def method_headers( + headers: dict[str, str], + method: str, + has_custom_transport: bool, +) -> dict[str, str]: + if has_custom_transport: + return user_headers(headers) + return {**headers, "X-ALEO-METHOD": method} + + +def jwt_origin(host: str) -> str: + """Derive the JWT origin from a host URL (scheme+host+port, no path).""" + parsed = urlparse(host) + return f"{parsed.scheme}://{parsed.netloc}" + + +def now_ms() -> int: + return int(time.time() * 1000) + + +def jwt_expired(jwt_data: dict[str, Any]) -> bool: + """Return True if jwt_data is missing or expiring within 5 minutes.""" + if not jwt_data: + return True + exp: int = jwt_data.get("expiration", 0) + return now_ms() >= exp - FIVE_MINUTES_MS + + +def validate_block_range(start: int, end: int) -> None: + if start < 0: + raise ValueError("start must be >= 0") + if start > end: + raise ValueError("start must be <= end") + if end - start > 50: + raise ValueError("Block range cannot exceed 50 blocks") + + +def strip_quotes(s: str) -> str: + """Strip quote characters (TS semantics for deployment tx IDs).""" + return s.replace('"', "") + + +def retry_with_backoff( + fn: Any, + *, + attempts: int = 5, + base_delay: float = 0.1, +) -> Any: + """Retry fn() up to `attempts` times on AleoNetworkError with status>=500.""" + last_err: Exception | None = None + for n in range(attempts): + try: + return fn() + except AleoNetworkError as exc: + if exc.status is not None and exc.status >= 500: + last_err = exc + delay = base_delay * (2 ** n) + random.uniform(0, 0.05) + time.sleep(delay) + else: + raise + raise last_err # type: ignore[misc] + + +async def async_retry_with_backoff( + fn: Any, + *, + attempts: int = 5, + base_delay: float = 0.1, +) -> Any: + """Async version of retry_with_backoff.""" + import asyncio + last_err: Exception | None = None + for n in range(attempts): + try: + return await fn() + except AleoNetworkError as exc: + if exc.status is not None and exc.status >= 500: + last_err = exc + delay = base_delay * (2 ** n) + random.uniform(0, 0.05) + await asyncio.sleep(delay) + else: + raise + raise last_err # type: ignore[misc] diff --git a/sdk/python/aleo/async_network_client.py b/sdk/python/aleo/async_network_client.py new file mode 100644 index 0000000..d8af249 --- /dev/null +++ b/sdk/python/aleo/async_network_client.py @@ -0,0 +1,455 @@ +"""Asynchronous Aleo network client (httpx-based).""" +from __future__ import annotations + +import json +from typing import Any + +from ._client_common import ( + DEFAULT_HOST, + DEFAULT_NETWORK, + AleoNetworkError, + AleoProvingError, + jwt_expired, + jwt_origin, + make_default_headers, + method_headers, + strip_quotes, + validate_block_range, + async_retry_with_backoff, +) +from .security import encrypt_proving_request + + +class AsyncAleoNetworkClient: + """Asynchronous client for the Aleo REST API (httpx-based).""" + + def __init__( + self, + host: str = DEFAULT_HOST, + *, + network: str = DEFAULT_NETWORK, + headers: dict[str, str] | None = None, + prover_uri: str | None = None, + record_scanner_uri: str | None = None, + transport: Any = None, + api_key: str | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + ) -> None: + try: + import httpx # type: ignore[import] + except ImportError: + raise ImportError( + "httpx is required for AsyncAleoNetworkClient. " + "Install with: pip install aleo[async]" + ) from None + + self._base_url: str = host + self._network: str = network + self._host: str = f"{host}/{network}" + self._has_custom_transport: bool = transport is not None + self._transport = transport + self._account: Any = None + self._verbose_errors: bool = True + self.api_key: str | None = api_key + self.consumer_id: str | None = consumer_id + self.jwt_data: dict[str, Any] | None = jwt_data + self._prover_uri: str | None = f"{prover_uri}/{network}" if prover_uri else None + self._record_scanner_uri: str | None = ( + f"{record_scanner_uri}/{network}" if record_scanner_uri else None + ) + + if self._has_custom_transport: + self.headers: dict[str, str] = dict(headers) if headers else {} + elif headers is not None: + self.headers = dict(headers) + else: + self.headers = make_default_headers() + + self._client: Any = httpx.AsyncClient() + + # ── Mutators ────────────────────────────────────────────────────────── + + def set_host(self, host: str) -> None: + self._base_url = host + self._host = f"{host}/{self._network}" + + def set_prover_uri(self, prover_uri: str) -> None: + self._prover_uri = f"{prover_uri}/{self._network}" + + def set_record_scanner_uri(self, record_scanner_uri: str) -> None: + self._record_scanner_uri = f"{record_scanner_uri}/{self._network}" + + def set_account(self, account: Any) -> None: + self._account = account + + def get_account(self) -> Any: + return self._account + + def set_header(self, name: str, value: str) -> None: + self.headers[name] = value + + def remove_header(self, name: str) -> None: + self.headers.pop(name, None) + + def set_verbose_errors(self, verbose: bool) -> None: + self._verbose_errors = verbose + + # ── Internal HTTP ───────────────────────────────────────────────────── + + def _request_headers(self, method_name: str) -> dict[str, str]: + return method_headers(self.headers, method_name, self._has_custom_transport) + + async def _get(self, path: str, method_name: str) -> Any: + url = self._host + path + hdrs = self._request_headers(method_name) + + async def _do() -> Any: + resp = await self._client.get(url, headers=hdrs) + if not resp.is_success: + raise AleoNetworkError( + f"GET {url} returned {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + return resp.json() + + return await async_retry_with_backoff(_do) + + async def _get_raw(self, path: str, method_name: str) -> str: + url = self._host + path + hdrs = self._request_headers(method_name) + + async def _do() -> str: + resp = await self._client.get(url, headers=hdrs) + if not resp.is_success: + raise AleoNetworkError( + f"GET {url} returned {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + return resp.text + + return await async_retry_with_backoff(_do) + + async def _post( + self, + url: str, + body: str, + method_name: str, + extra_headers: dict[str, str] | None = None, + ) -> Any: + hdrs = { + **self._request_headers(method_name), + "Content-Type": "application/json", + **(extra_headers or {}), + } + + async def _do() -> Any: + resp = await self._client.post(url, content=body.encode(), headers=hdrs) + if not resp.is_success: + raise AleoNetworkError( + f"POST {url} returned {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + return resp + + return await async_retry_with_backoff(_do) + + # ── JWT refresh ─────────────────────────────────────────────────────── + + async def _refresh_jwt(self, api_key: str, consumer_id: str) -> dict[str, Any]: + origin = jwt_origin(self._base_url) + url = f"{origin}/jwts/{consumer_id}" + hdrs = { + **self._request_headers("refreshJwt"), + "X-Provable-API-Key": api_key, + } + resp = await self._client.post(url, headers=hdrs) + if not resp.is_success: + raise AleoNetworkError( + f"JWT refresh failed: {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + auth = resp.headers.get("authorization") or resp.headers.get("Authorization") + if not auth: + raise AleoNetworkError("No authorization header in JWT refresh response") + body = resp.json() + return {"jwt": auth, "expiration": body["exp"] * 1000} + + async def _ensure_jwt( + self, + api_key: str | None, + consumer_id: str | None, + jwt_data: dict[str, Any] | None, + ) -> dict[str, Any] | None: + if jwt_data and not jwt_expired(jwt_data): + return jwt_data + resolved_key = api_key or self.api_key + resolved_cid = consumer_id or self.consumer_id + if resolved_key and resolved_cid: + new_jwt = await self._refresh_jwt(resolved_key, resolved_cid) + self.jwt_data = new_jwt + return new_jwt + return jwt_data + + # ── Block endpoints ─────────────────────────────────────────────────── + + async def get_block(self, height: int) -> Any: + return await self._get(f"/block/{height}", "getBlock") + + async def get_block_by_hash(self, block_hash: str) -> Any: + return await self._get(f"/block/{block_hash}", "getBlockByHash") + + async def get_block_range(self, start: int, end: int) -> list[Any]: + validate_block_range(start, end) + return await self._get(f"/blocks?start={start}&end={end}", "getBlockRange") + + async def get_latest_block(self) -> Any: + return await self._get("/block/latest", "getLatestBlock") + + async def get_latest_height(self) -> int: + return int(await self._get("/block/height/latest", "getLatestHeight")) + + async def get_latest_block_hash(self) -> str: + return str(await self._get("/block/hash/latest", "getLatestBlockHash")) + + async def get_latest_committee(self) -> Any: + return await self._get("/committee/latest", "getLatestCommittee") + + async def get_committee_by_height(self, height: int) -> Any: + return await self._get(f"/committee/{height}", "getCommitteeByHeight") + + async def get_state_root(self) -> str: + return str(await self._get("/stateRoot/latest", "getStateRoot")) + + async def get_state_paths(self, commitments: list[str]) -> list[Any]: + csv = ",".join(commitments) + return await self._get(f"/statePaths?commitments={csv}", "getStatePaths") + + # ── Program endpoints ───────────────────────────────────────────────── + + async def get_program(self, program_id: str, edition: int | None = None) -> str: + if edition is not None: + return await self._get(f"/program/{program_id}/{edition}", "getProgramVersion") + return await self._get(f"/program/{program_id}", "getProgramVersion") + + async def get_latest_program_edition(self, program_id: str) -> int: + raw = await self._get_raw(f"/program/{program_id}/latest_edition", "getLatestProgramEdition") + return int(json.loads(raw)) + + async def get_program_amendment_count(self, program_id: str) -> Any: + raw = await self._get_raw(f"/program/{program_id}/amendment_count", "getProgramAmendmentCount") + return json.loads(raw) + + async def get_program_mapping_names(self, program_id: str) -> list[str]: + return await self._get(f"/program/{program_id}/mappings", "getProgramMappingNames") + + async def get_program_mapping_value( + self, program_id: str, mapping_name: str, key: str + ) -> str: + return await self._get( + f"/program/{program_id}/mapping/{mapping_name}/{key}", + "getProgramMappingValue", + ) + + async def get_public_balance(self, address: str) -> int: + try: + val = await self.get_program_mapping_value("credits.aleo", "account", address) + return int(val) if val else 0 + except AleoNetworkError: + return 0 + + # ── Transaction endpoints ───────────────────────────────────────────── + + async def get_transaction(self, tx_id: str) -> Any: + return await self._get(f"/transaction/{tx_id}", "getTransaction") + + async def get_confirmed_transaction(self, tx_id: str) -> Any: + return await self._get(f"/transaction/confirmed/{tx_id}", "getConfirmedTransaction") + + async def get_transactions(self, block_height: int) -> list[Any]: + return await self._get(f"/block/{block_height}/transactions", "getTransactions") + + async def get_transactions_in_mempool(self) -> list[Any]: + return await self._get("/memoryPool/transactions", "getTransactionsInMempool") + + async def get_transition_id(self, input_or_output_id: str) -> str: + return await self._get( + f"/find/transitionID/{input_or_output_id}", "getTransitionId" + ) + + async def get_deployment_transaction_id_for_program(self, program_id: str) -> str: + raw = await self._get( + f"/find/transactionID/deployment/{program_id}", + "getDeploymentTransactionIDForProgram", + ) + return strip_quotes(str(raw)) + + async def get_deployment_transaction_for_program(self, program_id: str) -> Any: + tx_id = await self.get_deployment_transaction_id_for_program(program_id) + return await self.get_transaction(tx_id) + + # ── POST endpoints ──────────────────────────────────────────────────── + + async def submit_transaction(self, transaction: Any) -> str: + tx_str = str(transaction) + endpoint = ( + f"{self._host}/transaction/broadcast?check_transaction=true" + if self._verbose_errors + else f"{self._host}/transaction/broadcast" + ) + resp = await self._post(endpoint, tx_str, "submitTransaction") + return json.loads(resp.text) + + async def submit_solution(self, solution: str) -> str: + resp = await self._post( + f"{self._host}/solution/broadcast", solution, "submitSolution" + ) + return json.loads(resp.text) + + # ── Wait for confirmation ───────────────────────────────────────────── + + async def wait_for_transaction_confirmation( + self, + tx_id: str, + check_interval: float = 2.0, + timeout: float = 45.0, + ) -> Any: + import asyncio + import time as _time + + start = _time.monotonic() + url = f"{self._host}/transaction/confirmed/{tx_id}" + hdrs = self._request_headers("waitForTransactionConfirmation") + + while True: + elapsed = _time.monotonic() - start + if elapsed > timeout: + raise TimeoutError( + f"Transaction {tx_id} did not appear after {timeout}s" + ) + try: + resp = await self._client.get(url, headers=hdrs) + if not resp.is_success: + text = resp.text + if resp.status_code >= 400 and resp.status_code < 500 and "Invalid URL" in text: + raise AleoNetworkError( + f"Malformed transaction ID: {text}", status=resp.status_code + ) + await asyncio.sleep(check_interval) + continue + data = resp.json() + status = data.get("status") + if status == "accepted": + return data + if status == "rejected": + raise AleoNetworkError( + f"Transaction {tx_id} was rejected by the network" + ) + except AleoNetworkError: + raise + except Exception: + pass + await asyncio.sleep(check_interval) + + # ── DPS ─────────────────────────────────────────────────────────────── + + async def submit_proving_request_safe( + self, + proving_request: Any, + *, + url: str | None = None, + api_key: str | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + ) -> dict[str, Any]: + prover_uri = url or self._prover_uri or self._host + resolved_jwt = jwt_data or self.jwt_data + resolved_jwt = await self._ensure_jwt(api_key, consumer_id, resolved_jwt) + + hdrs: dict[str, str] = { + **self._request_headers("submitProvingRequest"), + "Content-Type": "application/json", + } + if resolved_jwt and resolved_jwt.get("jwt"): + hdrs["Authorization"] = resolved_jwt["jwt"] + + try: + from .mainnet import ProvingRequest # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + + if isinstance(proving_request, str): + pr_obj = ProvingRequest.from_string(proving_request) + else: + pr_obj = proving_request + + kind = pr_obj.kind() + endpoint = "/prove/request" if kind == "request" else "/prove/authorization" + + async def _send_once() -> dict[str, Any]: + pk_resp = await self._client.get(f"{prover_uri}/pubkey", headers=hdrs) + if not pk_resp.is_success: + raise AleoNetworkError( + f"Failed to fetch pubkey: {pk_resp.status_code}", + status=pk_resp.status_code, + ) + pk_data = pk_resp.json() + key_id = pk_data["key_id"] + public_key = pk_data["public_key"] + cookie = pk_resp.headers.get("set-cookie") + + pr_bytes = bytes(pr_obj.bytes()) + ciphertext = encrypt_proving_request(public_key, pr_bytes) + payload = json.dumps({"key_id": key_id, "ciphertext": ciphertext}) + post_hdrs = dict(hdrs) + if cookie: + post_hdrs["Cookie"] = cookie + + resp = await self._client.post( + f"{prover_uri}{endpoint}", + content=payload.encode(), + headers=post_hdrs, + ) + + if resp.status_code == 200: + return {"ok": True, "data": resp.json()} + elif resp.status_code in (400, 500, 503): + try: + err_body = resp.json() + msg = err_body.get("message", resp.text) + except Exception: + msg = resp.text + if resp.status_code in (500, 503): + raise AleoNetworkError(msg, status=resp.status_code) + return {"ok": False, "status": resp.status_code, "error": {"message": msg}} + else: + return {"ok": False, "status": resp.status_code, "error": {"message": resp.text}} + + try: + return await async_retry_with_backoff(_send_once) + except AleoNetworkError as exc: + return {"ok": False, "status": exc.status or 500, "error": {"message": str(exc)}} + + async def submit_proving_request( + self, + proving_request: Any, + *, + url: str | None = None, + api_key: str | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + ) -> Any: + result = await self.submit_proving_request_safe( + proving_request, + url=url, + api_key=api_key, + consumer_id=consumer_id, + jwt_data=jwt_data, + ) + if result["ok"]: + return result["data"] + err = result.get("error", {}) + raise AleoProvingError( + err.get("message", "Proving failed"), + status=result.get("status"), + ) diff --git a/sdk/python/aleo/network_client.py b/sdk/python/aleo/network_client.py new file mode 100644 index 0000000..f511911 --- /dev/null +++ b/sdk/python/aleo/network_client.py @@ -0,0 +1,532 @@ +"""Synchronous Aleo network client (requests-based).""" +from __future__ import annotations + +import json +from typing import Any + +import requests + +from ._client_common import ( + DEFAULT_HOST, + DEFAULT_NETWORK, + AleoNetworkError, + AleoProvingError, + jwt_expired, + jwt_origin, + make_default_headers, + method_headers, + strip_quotes, + validate_block_range, + retry_with_backoff, +) +from .security import encrypt_proving_request + + +class AleoNetworkClient: + """Synchronous client for the Aleo REST API.""" + + def __init__( + self, + host: str = DEFAULT_HOST, + *, + network: str = DEFAULT_NETWORK, + headers: dict[str, str] | None = None, + prover_uri: str | None = None, + record_scanner_uri: str | None = None, + transport: Any = None, + api_key: str | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + ) -> None: + self._base_url: str = host # versioned API root (e.g. https://api.provable.com/v2) + self._network: str = network + self._host: str = f"{host}/{network}" # full base for node endpoints + self._has_custom_transport: bool = transport is not None + self._transport: Any = transport # unused in sync; kept for parity + self._account: Any = None + self._verbose_errors: bool = True + self.api_key: str | None = api_key + self.consumer_id: str | None = consumer_id + self.jwt_data: dict[str, Any] | None = jwt_data + self._prover_uri: str | None = f"{prover_uri}/{network}" if prover_uri else None + self._record_scanner_uri: str | None = ( + f"{record_scanner_uri}/{network}" if record_scanner_uri else None + ) + + if self._has_custom_transport: + self.headers: dict[str, str] = ( + dict(headers) if headers else {} + ) + elif headers is not None: + self.headers = dict(headers) + else: + self.headers = make_default_headers() + + self._session: requests.Session = requests.Session() + + # ── Mutators ────────────────────────────────────────────────────────── + + def set_host(self, host: str) -> None: + self._base_url = host + self._host = f"{host}/{self._network}" + + def set_prover_uri(self, prover_uri: str) -> None: + self._prover_uri = f"{prover_uri}/{self._network}" + + def set_record_scanner_uri(self, record_scanner_uri: str) -> None: + self._record_scanner_uri = f"{record_scanner_uri}/{self._network}" + + def set_account(self, account: Any) -> None: + self._account = account + + def get_account(self) -> Any: + return self._account + + def set_header(self, name: str, value: str) -> None: + self.headers[name] = value + + def remove_header(self, name: str) -> None: + self.headers.pop(name, None) + + def set_verbose_errors(self, verbose: bool) -> None: + self._verbose_errors = verbose + + # ── Internal HTTP ───────────────────────────────────────────────────── + + def _request_headers(self, method_name: str) -> dict[str, str]: + return method_headers(self.headers, method_name, self._has_custom_transport) + + def _get(self, path: str, method_name: str) -> Any: + url = self._host + path + hdrs = self._request_headers(method_name) + + def _do() -> Any: + resp = self._session.get(url, headers=hdrs) + if not resp.ok: + raise AleoNetworkError( + f"GET {url} returned {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + return resp.json() + + return retry_with_backoff(_do) + + def _get_raw(self, path: str, method_name: str) -> str: + url = self._host + path + hdrs = self._request_headers(method_name) + + def _do() -> str: + resp = self._session.get(url, headers=hdrs) + if not resp.ok: + raise AleoNetworkError( + f"GET {url} returned {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + return resp.text + + return retry_with_backoff(_do) + + def _post( + self, + url: str, + body: str, + method_name: str, + extra_headers: dict[str, str] | None = None, + ) -> requests.Response: + hdrs = { + **self._request_headers(method_name), + "Content-Type": "application/json", + **(extra_headers or {}), + } + + def _do() -> requests.Response: + resp = self._session.post(url, data=body.encode(), headers=hdrs) + if not resp.ok: + raise AleoNetworkError( + f"POST {url} returned {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + return resp + + return retry_with_backoff(_do) + + # ── JWT refresh ─────────────────────────────────────────────────────── + + def _refresh_jwt(self, api_key: str, consumer_id: str) -> dict[str, Any]: + origin = jwt_origin(self._base_url) + url = f"{origin}/jwts/{consumer_id}" + hdrs = { + **self._request_headers("refreshJwt"), + "X-Provable-API-Key": api_key, + } + resp = self._session.post(url, headers=hdrs) + if not resp.ok: + raise AleoNetworkError( + f"JWT refresh failed: {resp.status_code}: {resp.text}", + status=resp.status_code, + ) + auth = resp.headers.get("authorization") or resp.headers.get("Authorization") + if not auth: + raise AleoNetworkError("No authorization header in JWT refresh response") + body = resp.json() + return {"jwt": auth, "expiration": body["exp"] * 1000} + + def _ensure_jwt( + self, + api_key: str | None, + consumer_id: str | None, + jwt_data: dict[str, Any] | None, + ) -> dict[str, Any] | None: + if jwt_data and not jwt_expired(jwt_data): + return jwt_data + resolved_key = api_key or self.api_key + resolved_cid = consumer_id or self.consumer_id + if resolved_key and resolved_cid: + new_jwt = self._refresh_jwt(resolved_key, resolved_cid) + self.jwt_data = new_jwt + return new_jwt + return jwt_data + + # ── Block endpoints ─────────────────────────────────────────────────── + + def get_block(self, height: int) -> Any: + return self._get(f"/block/{height}", "getBlock") + + def get_block_by_hash(self, block_hash: str) -> Any: + return self._get(f"/block/{block_hash}", "getBlockByHash") + + def get_block_range(self, start: int, end: int) -> list[Any]: + validate_block_range(start, end) + return self._get(f"/blocks?start={start}&end={end}", "getBlockRange") + + def get_latest_block(self) -> Any: + return self._get("/block/latest", "getLatestBlock") + + def get_latest_height(self) -> int: + return int(self._get("/block/height/latest", "getLatestHeight")) + + def get_latest_block_hash(self) -> str: + return str(self._get("/block/hash/latest", "getLatestBlockHash")) + + def get_latest_committee(self) -> Any: + return self._get("/committee/latest", "getLatestCommittee") + + def get_committee_by_height(self, height: int) -> Any: + return self._get(f"/committee/{height}", "getCommitteeByHeight") + + def get_state_root(self) -> str: + return str(self._get("/stateRoot/latest", "getStateRoot")) + + def get_state_paths(self, commitments: list[str]) -> list[Any]: + csv = ",".join(commitments) + return self._get(f"/statePaths?commitments={csv}", "getStatePaths") + + # ── Program endpoints ───────────────────────────────────────────────── + + def get_program(self, program_id: str, edition: int | None = None) -> str: + if edition is not None: + return self._get(f"/program/{program_id}/{edition}", "getProgramVersion") + return self._get(f"/program/{program_id}", "getProgramVersion") + + def get_latest_program_edition(self, program_id: str) -> int: + raw = self._get_raw(f"/program/{program_id}/latest_edition", "getLatestProgramEdition") + return int(json.loads(raw)) + + def get_program_amendment_count(self, program_id: str) -> Any: + raw = self._get_raw(f"/program/{program_id}/amendment_count", "getProgramAmendmentCount") + return json.loads(raw) + + def get_program_object(self, program_id: str, edition: int | None = None) -> Any: + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + source = self.get_program(program_id, edition) + return Program.from_source(source) + + def get_program_imports( + self, + program_id: str, + imports: dict[str, str] | None = None, + ) -> dict[str, str]: + if imports is None: + imports = {} + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + + source = self.get_program(program_id) + return self._collect_program_imports(source, imports) + + def _collect_program_imports( + self, + source: str, + imports: dict[str, str], + ) -> dict[str, str]: + """DFS import collection — source already fetched, no re-fetch.""" + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + prog = Program.from_source(source) + for imp_id_obj in prog.imports: + imp_id = str(imp_id_obj) + if imp_id not in imports: + imp_source = self.get_program(imp_id) + # Recurse into nested imports before recording this one + self._collect_program_imports(imp_source, imports) + imports[imp_id] = imp_source + return imports + + def get_program_import_names(self, program_id: str) -> list[str]: + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + source = self.get_program(program_id) + prog = Program.from_source(source) + return [str(imp) for imp in prog.imports] + + def get_program_mapping_names(self, program_id: str) -> list[str]: + return self._get(f"/program/{program_id}/mappings", "getProgramMappingNames") + + def get_program_mapping_value( + self, program_id: str, mapping_name: str, key: str + ) -> str: + return self._get( + f"/program/{program_id}/mapping/{mapping_name}/{key}", + "getProgramMappingValue", + ) + + def get_program_mapping_plaintext( + self, program_id: str, mapping_name: str, key: str + ) -> Any: + try: + from .mainnet import Plaintext # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + raw = self._get_raw( + f"/program/{program_id}/mapping/{mapping_name}/{key}", + "getProgramMappingPlaintext", + ) + return Plaintext.from_string(json.loads(raw)) + + def get_public_balance(self, address: str) -> int: + try: + val = self.get_program_mapping_value("credits.aleo", "account", address) + return int(val) if val else 0 + except AleoNetworkError: + return 0 + + # ── Transaction endpoints ───────────────────────────────────────────── + + def get_transaction(self, tx_id: str) -> Any: + return self._get(f"/transaction/{tx_id}", "getTransaction") + + def get_confirmed_transaction(self, tx_id: str) -> Any: + return self._get(f"/transaction/confirmed/{tx_id}", "getConfirmedTransaction") + + def get_transaction_object(self, tx_id: str) -> Any: + try: + from .mainnet import Transaction # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + raw = self._get_raw(f"/transaction/{tx_id}", "getTransactionObject") + return Transaction.from_json(raw) + + def get_transactions(self, block_height: int) -> list[Any]: + return self._get(f"/block/{block_height}/transactions", "getTransactions") + + def get_transactions_in_mempool(self) -> list[Any]: + return self._get("/memoryPool/transactions", "getTransactionsInMempool") + + def get_transition_id(self, input_or_output_id: str) -> str: + return self._get( + f"/find/transitionID/{input_or_output_id}", "getTransitionId" + ) + + def get_deployment_transaction_id_for_program(self, program_id: str) -> str: + raw = self._get( + f"/find/transactionID/deployment/{program_id}", + "getDeploymentTransactionIDForProgram", + ) + return strip_quotes(str(raw)) + + def get_deployment_transaction_for_program(self, program_id: str) -> Any: + tx_id = self.get_deployment_transaction_id_for_program(program_id) + return self.get_transaction(tx_id) + + # ── POST endpoints ──────────────────────────────────────────────────── + + def submit_transaction(self, transaction: Any) -> str: + tx_str = str(transaction) + endpoint = ( + f"{self._host}/transaction/broadcast?check_transaction=true" + if self._verbose_errors + else f"{self._host}/transaction/broadcast" + ) + resp = self._post(endpoint, tx_str, "submitTransaction") + return json.loads(resp.text) + + def submit_solution(self, solution: str) -> str: + resp = self._post( + f"{self._host}/solution/broadcast", solution, "submitSolution" + ) + return json.loads(resp.text) + + # ── Wait for confirmation ───────────────────────────────────────────── + + def wait_for_transaction_confirmation( + self, + tx_id: str, + check_interval: float = 2.0, + timeout: float = 45.0, + ) -> Any: + import time as _time + + start = _time.monotonic() + url = f"{self._host}/transaction/confirmed/{tx_id}" + hdrs = self._request_headers("waitForTransactionConfirmation") + + while True: + elapsed = _time.monotonic() - start + if elapsed > timeout: + raise TimeoutError( + f"Transaction {tx_id} did not appear after {timeout}s" + ) + try: + resp = self._session.get(url, headers=hdrs) + if not resp.ok: + text = resp.text + if resp.status_code >= 400 and resp.status_code < 500 and "Invalid URL" in text: + raise AleoNetworkError(f"Malformed transaction ID: {text}", status=resp.status_code) + _time.sleep(check_interval) + continue + data = resp.json() + status = data.get("status") + if status == "accepted": + return data + if status == "rejected": + raise AleoNetworkError( + f"Transaction {tx_id} was rejected by the network" + ) + except AleoNetworkError: + raise + except Exception: + pass + _time.sleep(check_interval) + + # ── DPS: submit_proving_request_safe / submit_proving_request ───────── + + def submit_proving_request_safe( + self, + proving_request: Any, + *, + url: str | None = None, + api_key: str | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Submit a proving request, returning {ok, data|error, status}. Never raises on HTTP errors.""" + # Determine the prover URI + prover_uri = url or self._prover_uri or self._host + + # Resolve JWT + resolved_jwt = jwt_data or self.jwt_data + resolved_jwt = self._ensure_jwt(api_key, consumer_id, resolved_jwt) + + # Build headers + hdrs: dict[str, str] = { + **self._request_headers("submitProvingRequest"), + "Content-Type": "application/json", + } + if resolved_jwt and resolved_jwt.get("jwt"): + hdrs["Authorization"] = resolved_jwt["jwt"] + + # Parse the proving request to determine routing + try: + from .mainnet import ProvingRequest # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + + if isinstance(proving_request, str): + pr_obj = ProvingRequest.from_string(proving_request) + else: + pr_obj = proving_request + + kind = pr_obj.kind() + endpoint = "/prove/request" if kind == "request" else "/prove/authorization" + + def _send_once() -> dict[str, Any]: + # Fetch pubkey + session cookie + pk_resp = self._session.get(f"{prover_uri}/pubkey", headers=hdrs) + if not pk_resp.ok: + raise AleoNetworkError( + f"Failed to fetch pubkey: {pk_resp.status_code}", + status=pk_resp.status_code, + ) + pk_data = pk_resp.json() + key_id = pk_data["key_id"] + public_key = pk_data["public_key"] + cookie = pk_resp.headers.get("set-cookie") + + # Encrypt + pr_bytes = bytes(pr_obj.bytes()) + ciphertext = encrypt_proving_request(public_key, pr_bytes) + + payload = json.dumps({"key_id": key_id, "ciphertext": ciphertext}) + post_hdrs = dict(hdrs) + if cookie: + post_hdrs["Cookie"] = cookie + + resp = self._session.post( + f"{prover_uri}{endpoint}", + data=payload.encode(), + headers=post_hdrs, + ) + + if resp.status_code == 200: + body = resp.json() + return {"ok": True, "data": body} + elif resp.status_code in (400, 500, 503): + try: + err_body = resp.json() + msg = err_body.get("message", resp.text) + except Exception: + msg = resp.text + if resp.status_code in (500, 503): + raise AleoNetworkError(msg, status=resp.status_code) + return {"ok": False, "status": resp.status_code, "error": {"message": msg}} + else: + return {"ok": False, "status": resp.status_code, "error": {"message": resp.text}} + + try: + return retry_with_backoff(_send_once) + except AleoNetworkError as exc: + return {"ok": False, "status": exc.status or 500, "error": {"message": str(exc)}} + + def submit_proving_request( + self, + proving_request: Any, + *, + url: str | None = None, + api_key: str | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + ) -> Any: + """Submit a proving request, raising AleoProvingError on failure.""" + result = self.submit_proving_request_safe( + proving_request, + url=url, + api_key=api_key, + consumer_id=consumer_id, + jwt_data=jwt_data, + ) + if result["ok"]: + return result["data"] + err = result.get("error", {}) + raise AleoProvingError( + err.get("message", "Proving failed"), + status=result.get("status"), + ) diff --git a/sdk/python/aleo/security.py b/sdk/python/aleo/security.py new file mode 100644 index 0000000..fa56afa --- /dev/null +++ b/sdk/python/aleo/security.py @@ -0,0 +1,24 @@ +"""PyNaCl sealed-box helpers for DPS encryption.""" +from __future__ import annotations + +import base64 + + +def encrypt_proving_request(public_key_b64: str, pr_bytes: bytes) -> str: + """Encrypt bytes with a Curve25519 public key using a NaCl sealed box. + + Returns standard (non-URL-safe) base64. + Raises ImportError with helpful message if pynacl is not installed. + """ + try: + from nacl.public import PublicKey, SealedBox # type: ignore[import] + except ImportError: + raise ImportError( + "PyNaCl is required for DPS encryption. " + "Install with: pip install aleo[dps]" + ) from None + + recipient_key = PublicKey(base64.b64decode(public_key_b64)) + box = SealedBox(recipient_key) + ciphertext = box.encrypt(pr_bytes) + return base64.b64encode(ciphertext).decode("ascii") diff --git a/sdk/python/tests/test_network_client.py b/sdk/python/tests/test_network_client.py new file mode 100644 index 0000000..e68d8e5 --- /dev/null +++ b/sdk/python/tests/test_network_client.py @@ -0,0 +1,671 @@ +"""Tests for AleoNetworkClient (sync, requests-based). All mocked — no live network.""" +from __future__ import annotations + +import json +from typing import Any +from unittest.mock import MagicMock, patch, call +import pytest +import responses as resp_lib +from responses import RequestsMock + +from aleo.network_client import AleoNetworkClient +from aleo._client_common import AleoNetworkError, jwt_origin + +BASE = "https://api.provable.com/v2" +NET = "mainnet" +HOST = f"{BASE}/{NET}" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def make_client(**kwargs: Any) -> AleoNetworkClient: + return AleoNetworkClient(BASE, network=NET, **kwargs) + + +# --------------------------------------------------------------------------- +# URL construction +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_get_block_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/42", json={"height": 42}) + c = make_client() + result = c.get_block(42) + assert result["height"] == 42 + assert resp_lib.calls[0].request.url == f"{HOST}/block/42" + + +@resp_lib.activate +def test_get_latest_block_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={"height": 100}) + c = make_client() + result = c.get_latest_block() + assert result["height"] == 100 + + +@resp_lib.activate +def test_get_latest_height_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/height/latest", json=9999) + c = make_client() + assert c.get_latest_height() == 9999 + + +@resp_lib.activate +def test_get_latest_block_hash_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/hash/latest", json="ab1234hash") + c = make_client() + assert c.get_latest_block_hash() == "ab1234hash" + + +@resp_lib.activate +def test_get_committee_by_height_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/committee/100", json={"members": []}) + c = make_client() + result = c.get_committee_by_height(100) + assert result == {"members": []} + + +@resp_lib.activate +def test_get_state_root_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/stateRoot/latest", json="sr1abc") + c = make_client() + assert c.get_state_root() == "sr1abc" + + +@resp_lib.activate +def test_get_program_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/program/hello.aleo", json="program hello.aleo;") + c = make_client() + result = c.get_program("hello.aleo") + assert "hello.aleo" in result + + +@resp_lib.activate +def test_get_program_with_edition_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/program/hello.aleo/2", json="program hello.aleo;") + c = make_client() + result = c.get_program("hello.aleo", edition=2) + assert "hello.aleo" in result + assert "/program/hello.aleo/2" in resp_lib.calls[0].request.url + + +@resp_lib.activate +def test_get_transaction_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/transaction/at1abc", json={"id": "at1abc"}) + c = make_client() + result = c.get_transaction("at1abc") + assert result["id"] == "at1abc" + + +@resp_lib.activate +def test_get_confirmed_transaction_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/transaction/confirmed/at1abc", json={"status": "accepted"}) + c = make_client() + result = c.get_confirmed_transaction("at1abc") + assert result["status"] == "accepted" + + +@resp_lib.activate +def test_get_mapping_value_url() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/program/credits.aleo/mapping/account/aleo1abc", json="500u64") + c = make_client() + val = c.get_program_mapping_value("credits.aleo", "account", "aleo1abc") + assert val == "500u64" + + +@resp_lib.activate +def test_get_deployment_tx_id_strips_quotes() -> None: + resp_lib.add( + resp_lib.GET, + f"{HOST}/find/transactionID/deployment/hello.aleo", + json='"at1realid"', + ) + c = make_client() + result = c.get_deployment_transaction_id_for_program("hello.aleo") + assert '"' not in result + assert "at1realid" in result + + +# --------------------------------------------------------------------------- +# Block range validation +# --------------------------------------------------------------------------- + +def test_block_range_negative_start_raises() -> None: + c = make_client() + with pytest.raises(ValueError, match="start"): + c.get_block_range(-1, 10) + + +def test_block_range_start_gt_end_raises() -> None: + c = make_client() + with pytest.raises(ValueError, match="start"): + c.get_block_range(50, 10) + + +def test_block_range_span_exceeds_50_raises() -> None: + c = make_client() + with pytest.raises(ValueError, match="50"): + c.get_block_range(0, 51) + + +@resp_lib.activate +def test_block_range_exactly_50_ok() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/blocks", json=[]) + c = make_client() + c.get_block_range(0, 50) # span=50, should not raise + + +# --------------------------------------------------------------------------- +# get_program_imports dedup (DFS, each program fetched exactly once) +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_get_program_imports_dedup() -> None: + """2-level import fixture: top->mid->leaf. Each fetched exactly once.""" + leaf_src = "program leaf.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + mid_src = f"import leaf.aleo;\nprogram mid.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + top_src = f"import mid.aleo;\nprogram top.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + + resp_lib.add(resp_lib.GET, f"{HOST}/program/top.aleo", json=top_src) + resp_lib.add(resp_lib.GET, f"{HOST}/program/mid.aleo", json=mid_src) + resp_lib.add(resp_lib.GET, f"{HOST}/program/leaf.aleo", json=leaf_src) + + c = make_client() + imports = c.get_program_imports("top.aleo") + + # Should contain both imports + assert "mid.aleo" in imports + assert "leaf.aleo" in imports + + # Each program fetched exactly once (dedup check via call count) + urls = [call.request.url.split("?")[0] for call in resp_lib.calls] + # top.aleo fetched once (during get_program_imports), mid.aleo once, leaf.aleo once + mid_count = sum(1 for u in urls if "/program/mid.aleo" in u) + leaf_count = sum(1 for u in urls if "/program/leaf.aleo" in u) + assert mid_count == 1, f"mid.aleo fetched {mid_count} times, expected 1" + assert leaf_count == 1, f"leaf.aleo fetched {leaf_count} times, expected 1" + + +# --------------------------------------------------------------------------- +# Retry: 503 retried, 400 not retried +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_retry_on_503() -> None: + """503s should be retried; succeed on 3rd attempt.""" + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", status=503) + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", status=503) + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={"height": 1}) + + c = make_client() + # Patch sleep to speed up test + with patch("aleo._client_common.time.sleep"): + result = c.get_latest_block() + assert result["height"] == 1 + assert len(resp_lib.calls) == 3 + + +@resp_lib.activate +def test_no_retry_on_400() -> None: + """400 errors should NOT be retried.""" + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", status=400, body="bad request") + + c = make_client() + with pytest.raises(AleoNetworkError) as exc_info: + c.get_latest_block() + assert exc_info.value.status == 400 + assert len(resp_lib.calls) == 1 + + +# --------------------------------------------------------------------------- +# Headers: default, per-method, custom transport suppression +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_default_sdk_headers_present() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={}) + c = make_client() + c.get_latest_block() + req_headers = resp_lib.calls[0].request.headers + assert "X-Aleo-SDK-Version" in req_headers + assert "X-Aleo-environment" in req_headers + assert req_headers["X-Aleo-environment"] == "python" + + +@resp_lib.activate +def test_per_method_header() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/42", json={}) + c = make_client() + c.get_block(42) + req_headers = resp_lib.calls[0].request.headers + assert req_headers.get("X-ALEO-METHOD") == "getBlock" + + +@resp_lib.activate +def test_custom_transport_suppresses_sdk_headers() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={}) + # Passing transport=True simulates custom transport (has_custom_transport=True) + c = AleoNetworkClient(BASE, network=NET, transport=object()) + c.get_latest_block() + req_headers = resp_lib.calls[0].request.headers + assert "X-Aleo-SDK-Version" not in req_headers + assert "X-ALEO-METHOD" not in req_headers + + +@resp_lib.activate +def test_set_header() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={}) + c = make_client() + c.set_header("X-Custom-Foo", "bar") + c.get_latest_block() + assert resp_lib.calls[0].request.headers.get("X-Custom-Foo") == "bar" + + +@resp_lib.activate +def test_remove_header() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={}) + c = make_client() + c.set_header("X-Custom-Foo", "bar") + c.remove_header("X-Custom-Foo") + c.get_latest_block() + assert "X-Custom-Foo" not in resp_lib.calls[0].request.headers + + +# --------------------------------------------------------------------------- +# JWT origin derivation +# --------------------------------------------------------------------------- + +def test_jwt_origin_standard() -> None: + assert jwt_origin("https://api.provable.com/v2") == "https://api.provable.com" + + +def test_jwt_origin_with_port() -> None: + assert jwt_origin("http://localhost:3030/v2") == "http://localhost:3030" + + +def test_jwt_origin_trailing_slash() -> None: + assert jwt_origin("https://api.provable.com/v2/") == "https://api.provable.com" + + +def test_jwt_origin_localhost() -> None: + assert jwt_origin("http://localhost/v1") == "http://localhost" + + +@resp_lib.activate +def test_jwt_fetched_from_correct_origin() -> None: + """JWT refresh hits {origin}/jwts/{consumer_id}.""" + jwt_url = "https://api.provable.com/jwts/cid123" + resp_lib.add( + resp_lib.POST, jwt_url, + json={"exp": 9999999999}, + headers={"authorization": "Bearer tok123"}, + ) + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={"height": 1}) + + c = make_client(api_key="key123", consumer_id="cid123") + # Force JWT refresh by leaving jwt_data None + with patch("aleo._client_common.time.sleep"): + jwt = c._refresh_jwt("key123", "cid123") + assert jwt["jwt"] == "Bearer tok123" + assert jwt["expiration"] == 9999999999 * 1000 + + +@resp_lib.activate +def test_jwt_from_response_header() -> None: + jwt_url = "https://api.provable.com/jwts/cid999" + resp_lib.add( + resp_lib.POST, jwt_url, + json={"exp": 9999999999}, + headers={"authorization": "Bearer mytoken"}, + ) + c = make_client(api_key="mykey", consumer_id="cid999") + jwt = c._refresh_jwt("mykey", "cid999") + assert jwt["jwt"] == "Bearer mytoken" + + +@resp_lib.activate +def test_jwt_expiry_within_5min_triggers_refresh() -> None: + """If JWT expires within 5 minutes, refresh is triggered.""" + import time + # Set expiration to 2 min from now (within 5 min window) + exp_ms = int(time.time() * 1000) + 2 * 60 * 1000 + jwt_url = "https://api.provable.com/jwts/cid1" + resp_lib.add( + resp_lib.POST, jwt_url, + json={"exp": 9999999999}, + headers={"authorization": "Bearer refreshed"}, + ) + resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={"height": 1}) + + c = make_client(api_key="k", consumer_id="cid1") + c.jwt_data = {"jwt": "Bearer old", "expiration": exp_ms} + # _ensure_jwt should detect near-expiry and refresh + new_jwt = c._ensure_jwt("k", "cid1", c.jwt_data) + assert new_jwt is not None + assert new_jwt["jwt"] == "Bearer refreshed" + + +# --------------------------------------------------------------------------- +# submit_transaction: verbose_errors query param +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_submit_transaction_verbose_errors_true() -> None: + resp_lib.add(resp_lib.POST, f"{HOST}/transaction/broadcast", json="at1txid") + c = make_client() + c.set_verbose_errors(True) + result = c.submit_transaction('{"fake": "tx"}') + url = resp_lib.calls[0].request.url + assert "check_transaction=true" in url + assert result == "at1txid" + + +@resp_lib.activate +def test_submit_transaction_verbose_errors_false() -> None: + resp_lib.add(resp_lib.POST, f"{HOST}/transaction/broadcast", json="at1txid") + c = make_client() + c.set_verbose_errors(False) + c.submit_transaction('{"fake": "tx"}') + url = resp_lib.calls[0].request.url + assert "check_transaction" not in url + + +# --------------------------------------------------------------------------- +# wait_for_transaction_confirmation +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_wait_for_confirmation_accepted() -> None: + resp_lib.add( + resp_lib.GET, f"{HOST}/transaction/confirmed/at1abc", + json={"status": "accepted", "id": "at1abc"}, + ) + c = make_client() + result = c.wait_for_transaction_confirmation("at1abc", check_interval=0.01, timeout=5.0) + assert result["status"] == "accepted" + + +@resp_lib.activate +def test_wait_for_confirmation_rejected() -> None: + resp_lib.add( + resp_lib.GET, f"{HOST}/transaction/confirmed/at1rej", + json={"status": "rejected"}, + ) + c = make_client() + with pytest.raises(AleoNetworkError, match="rejected"): + c.wait_for_transaction_confirmation("at1rej", check_interval=0.01, timeout=5.0) + + +@resp_lib.activate +def test_wait_for_confirmation_timeout() -> None: + resp_lib.add(resp_lib.GET, f"{HOST}/transaction/confirmed/at1slow", status=404) + c = make_client() + with pytest.raises(TimeoutError): + c.wait_for_transaction_confirmation("at1slow", check_interval=0.01, timeout=0.05) + + +@resp_lib.activate +def test_wait_for_confirmation_invalid_url_fatal() -> None: + resp_lib.add( + resp_lib.GET, f"{HOST}/transaction/confirmed/bad", + status=400, body="Invalid URL: bad transaction id", + ) + c = make_client() + with pytest.raises(AleoNetworkError, match="Malformed"): + c.wait_for_transaction_confirmation("bad", check_interval=0.01, timeout=5.0) + + +# --------------------------------------------------------------------------- +# DPS: submit_proving_request_safe / submit_proving_request +# --------------------------------------------------------------------------- + +def _load_proving_request() -> Any: + """Load the KAT proving request from vectors.""" + import json + from pathlib import Path + v = json.loads((Path(__file__).parent / "vectors" / "proving_request.json").read_text()) + pr_str = v["PUZZLE_SPINNER_V002_PROVING_REQUEST"] + from aleo.mainnet import ProvingRequest # type: ignore[attr-defined] + return ProvingRequest.from_string(pr_str) + + +@pytest.fixture +def nacl_keypair() -> Any: + """Generate a real X25519 keypair for DPS mock tests.""" + from nacl.public import PrivateKey + import base64 + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + return sk, pk_b64 + + +@pytest.mark.slow +@resp_lib.activate +def test_dps_authorization_variant_routes_correctly(nacl_keypair: Any) -> None: + """Authorization-variant PR hits /prove/authorization; ciphertext decrypts correctly.""" + from nacl.public import PrivateKey, SealedBox + import base64 + + sk, pk_b64 = nacl_keypair + prover = "https://prover.provable.prove" + + resp_lib.add( + resp_lib.GET, f"{prover}/mainnet/pubkey", + json={"key_id": "k1", "public_key": pk_b64}, + headers={"set-cookie": "session=abc"}, + ) + resp_lib.add( + resp_lib.POST, f"{prover}/mainnet/prove/authorization", + json={"transaction": "at1fake", "broadcast_result": {"status": "accepted"}}, + ) + + c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) + # Inject a non-expiring JWT so no refresh is triggered + c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} + + try: + pr = _load_proving_request() + except Exception: + pytest.skip("ProvingRequest WASM not available (compile in progress)") + + result = c.submit_proving_request_safe(pr) + assert result["ok"] is True + assert result["data"]["transaction"] == "at1fake" + + # Verify the POST hit /prove/authorization + post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] + assert any("prove/authorization" in c.request.url for c in post_calls) + + # Verify ciphertext decrypts to PR bytes + post_body = json.loads(post_calls[-1].request.body) + ciphertext = base64.b64decode(post_body["ciphertext"]) + box = SealedBox(sk) + decrypted = box.decrypt(ciphertext) + assert decrypted == bytes(pr.bytes()) + + +@pytest.mark.slow +@resp_lib.activate +def test_dps_400_returned_verbatim_not_retried(nacl_keypair: Any) -> None: + """400 from prover returned as ok=False; not retried.""" + _, pk_b64 = nacl_keypair + prover = "https://prover.provable.prove" + + resp_lib.add( + resp_lib.GET, f"{prover}/mainnet/pubkey", + json={"key_id": "k1", "public_key": pk_b64}, + ) + resp_lib.add( + resp_lib.POST, f"{prover}/mainnet/prove/authorization", + status=400, json={"message": "bad input"}, + ) + + c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) + c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} + + try: + pr = _load_proving_request() + except Exception: + pytest.skip("ProvingRequest WASM not available") + + result = c.submit_proving_request_safe(pr) + assert result["ok"] is False + assert result["status"] == 400 + # Only one POST call (not retried) + post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] + assert len(post_calls) == 1 + + +@pytest.mark.slow +@resp_lib.activate +def test_dps_503_retried(nacl_keypair: Any) -> None: + """503 from prover is retried.""" + _, pk_b64 = nacl_keypair + prover = "https://prover.provable.prove" + + # pubkey fetched each retry + for _ in range(3): + resp_lib.add( + resp_lib.GET, f"{prover}/mainnet/pubkey", + json={"key_id": "k1", "public_key": pk_b64}, + ) + resp_lib.add(resp_lib.POST, f"{prover}/mainnet/prove/authorization", status=503) + resp_lib.add(resp_lib.POST, f"{prover}/mainnet/prove/authorization", status=503) + resp_lib.add( + resp_lib.POST, f"{prover}/mainnet/prove/authorization", + json={"transaction": "at1ok", "broadcast_result": {"status": "accepted"}}, + ) + + c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) + c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} + + try: + pr = _load_proving_request() + except Exception: + pytest.skip("ProvingRequest WASM not available") + + with patch("aleo._client_common.time.sleep"): + result = c.submit_proving_request_safe(pr) + + assert result["ok"] is True + post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] + assert len(post_calls) == 3 + + +@pytest.mark.slow +@resp_lib.activate +def test_dps_cookie_echoed(nacl_keypair: Any) -> None: + """set-cookie from /pubkey is echoed back as Cookie on the prove POST.""" + _, pk_b64 = nacl_keypair + prover = "https://prover.provable.prove" + + resp_lib.add( + resp_lib.GET, f"{prover}/mainnet/pubkey", + json={"key_id": "k1", "public_key": pk_b64}, + headers={"set-cookie": "session=mysession"}, + ) + resp_lib.add( + resp_lib.POST, f"{prover}/mainnet/prove/authorization", + json={"transaction": "at1ok", "broadcast_result": {"status": "accepted"}}, + ) + + c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) + c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} + + try: + pr = _load_proving_request() + except Exception: + pytest.skip("ProvingRequest WASM not available") + + c.submit_proving_request_safe(pr) + post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] + assert post_calls[-1].request.headers.get("Cookie") == "session=mysession" + + +@pytest.mark.slow +@resp_lib.activate +def test_dps_authorization_header_sent(nacl_keypair: Any) -> None: + """JWT is sent as Authorization header on prove POST.""" + _, pk_b64 = nacl_keypair + prover = "https://prover.provable.prove" + + resp_lib.add( + resp_lib.GET, f"{prover}/mainnet/pubkey", + json={"key_id": "k1", "public_key": pk_b64}, + ) + resp_lib.add( + resp_lib.POST, f"{prover}/mainnet/prove/authorization", + json={"transaction": "at1ok", "broadcast_result": {"status": "accepted"}}, + ) + + c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) + c.jwt_data = {"jwt": "Bearer myjwt", "expiration": 99999999999999} + + try: + pr = _load_proving_request() + except Exception: + pytest.skip("ProvingRequest WASM not available") + + c.submit_proving_request_safe(pr) + post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] + assert post_calls[-1].request.headers.get("Authorization") == "Bearer myjwt" + + +@pytest.mark.slow +@resp_lib.activate +def test_dps_submit_proving_request_raises_on_failure(nacl_keypair: Any) -> None: + """submit_proving_request raises AleoProvingError on failure.""" + from aleo._client_common import AleoProvingError + _, pk_b64 = nacl_keypair + prover = "https://prover.provable.prove" + + resp_lib.add( + resp_lib.GET, f"{prover}/mainnet/pubkey", + json={"key_id": "k1", "public_key": pk_b64}, + ) + resp_lib.add( + resp_lib.POST, f"{prover}/mainnet/prove/authorization", + status=400, json={"message": "invalid request"}, + ) + + c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) + c.jwt_data = {"jwt": "Bearer jwt", "expiration": 99999999999999} + + try: + pr = _load_proving_request() + except Exception: + pytest.skip("ProvingRequest WASM not available") + + with pytest.raises(AleoProvingError) as exc_info: + c.submit_proving_request(pr) + assert exc_info.value.status == 400 + + +# --------------------------------------------------------------------------- +# Mutators +# --------------------------------------------------------------------------- + +def test_set_account() -> None: + c = make_client() + mock_acct = MagicMock() + c.set_account(mock_acct) + assert c.get_account() is mock_acct + + +def test_network_in_host() -> None: + c = AleoNetworkClient("https://api.provable.com/v2", network="testnet") + assert c._host == "https://api.provable.com/v2/testnet" + + +def test_set_host_updates_host() -> None: + c = make_client() + c.set_host("https://other.example.com/v3") + assert c._host == f"https://other.example.com/v3/{NET}" + + +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}" diff --git a/sdk/python/tests/test_network_client_async.py b/sdk/python/tests/test_network_client_async.py new file mode 100644 index 0000000..c67bd36 --- /dev/null +++ b/sdk/python/tests/test_network_client_async.py @@ -0,0 +1,380 @@ +"""Tests for AsyncAleoNetworkClient (async, httpx-based). All mocked — no live network.""" +from __future__ import annotations + +import json +import base64 +from typing import Any, Callable +from unittest.mock import patch +import pytest +import httpx + +from aleo.async_network_client import AsyncAleoNetworkClient +from aleo._client_common import AleoNetworkError, AleoProvingError, make_default_headers + +BASE = "https://api.provable.com/v2" +NET = "mainnet" +HOST = f"{BASE}/{NET}" + +pytestmark = pytest.mark.asyncio + + +# --------------------------------------------------------------------------- +# Mock transport helpers +# --------------------------------------------------------------------------- + +def route_handler( + routes: dict[str, Any], +) -> Callable[[httpx.Request], httpx.Response]: + """Return a handler function for httpx.MockTransport. + + Each key in routes is a URL substring to match; value is either an + httpx.Response or a callable(request) -> Response. + """ + def handler(request: httpx.Request) -> httpx.Response: + url = str(request.url) + for pattern, response in routes.items(): + if pattern in url: + if callable(response): + return response(request) + return response + return httpx.Response(404, text="not found") + return handler + + +def jr(data: Any, status: int = 200, headers: dict[str, str] | None = None) -> httpx.Response: + """Shorthand: JSON response.""" + return httpx.Response(status, json=data, headers=headers or {}) + + +def make_client( + routes: dict[str, Any] | None = None, + **overrides: Any, +) -> AsyncAleoNetworkClient: + """Build AsyncAleoNetworkClient bypassing __init__ to inject a mock transport.""" + transport = httpx.MockTransport(route_handler(routes or {})) + c: AsyncAleoNetworkClient = object.__new__(AsyncAleoNetworkClient) + c._base_url = BASE + c._network = NET + c._host = HOST + c._has_custom_transport = False + c._transport = None + c._account = None + c._verbose_errors = True + c.api_key = None + c.consumer_id = None + c.jwt_data = None + c._prover_uri = None + c._record_scanner_uri = None + c.headers = make_default_headers() + c._client = httpx.AsyncClient(transport=transport) + for k, v in overrides.items(): + setattr(c, k, v) + return c + + +def make_client_with_handler( + handler: Callable[[httpx.Request], httpx.Response], + **overrides: Any, +) -> AsyncAleoNetworkClient: + transport = httpx.MockTransport(handler) + c: AsyncAleoNetworkClient = object.__new__(AsyncAleoNetworkClient) + c._base_url = BASE + c._network = NET + c._host = HOST + c._has_custom_transport = False + c._transport = None + c._account = None + c._verbose_errors = True + c.api_key = None + c.consumer_id = None + c.jwt_data = None + c._prover_uri = None + c._record_scanner_uri = None + c.headers = make_default_headers() + c._client = httpx.AsyncClient(transport=transport) + for k, v in overrides.items(): + setattr(c, k, v) + return c + + +# --------------------------------------------------------------------------- +# URL construction +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_get_block() -> None: + c = make_client({f"{HOST}/block/42": jr({"height": 42})}) + result = await c.get_block(42) + assert result["height"] == 42 + + +@pytest.mark.asyncio +async def test_async_get_latest_height() -> None: + c = make_client({f"{HOST}/block/height/latest": jr(5000)}) + assert await c.get_latest_height() == 5000 + + +@pytest.mark.asyncio +async def test_async_get_latest_block_hash() -> None: + c = make_client({f"{HOST}/block/hash/latest": jr("hash123")}) + assert await c.get_latest_block_hash() == "hash123" + + +@pytest.mark.asyncio +async def test_async_get_state_root() -> None: + c = make_client({f"{HOST}/stateRoot/latest": jr("sr1abc")}) + assert await c.get_state_root() == "sr1abc" + + +@pytest.mark.asyncio +async def test_async_get_program() -> None: + c = make_client({f"{HOST}/program/hello.aleo": jr("program hello.aleo;")}) + result = await c.get_program("hello.aleo") + assert "hello.aleo" in result + + +@pytest.mark.asyncio +async def test_async_get_transaction() -> None: + c = make_client({f"{HOST}/transaction/at1abc": jr({"id": "at1abc"})}) + result = await c.get_transaction("at1abc") + assert result["id"] == "at1abc" + + +@pytest.mark.asyncio +async def test_async_get_confirmed_transaction() -> None: + c = make_client({f"{HOST}/transaction/confirmed/at1abc": jr({"status": "accepted"})}) + result = await c.get_confirmed_transaction("at1abc") + assert result["status"] == "accepted" + + +@pytest.mark.asyncio +async def test_async_get_deployment_tx_id_strips_quotes() -> None: + c = make_client({ + f"{HOST}/find/transactionID/deployment/hello.aleo": jr('"at1realid"'), + }) + result = await c.get_deployment_transaction_id_for_program("hello.aleo") + assert '"' not in result + assert "at1realid" in result + + +# --------------------------------------------------------------------------- +# Block range validation +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_block_range_negative_start_raises() -> None: + c = make_client() + with pytest.raises(ValueError, match="start"): + await c.get_block_range(-1, 10) + + +@pytest.mark.asyncio +async def test_async_block_range_span_exceeds_50_raises() -> None: + c = make_client() + with pytest.raises(ValueError, match="50"): + await c.get_block_range(0, 51) + + +# --------------------------------------------------------------------------- +# Retry on 5xx +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_retry_on_503() -> None: + call_count = 0 + + def handler(req: httpx.Request) -> httpx.Response: + nonlocal call_count + call_count += 1 + if call_count < 3: + return httpx.Response(503, text="service unavailable") + return jr({"height": 7}) + + c = make_client_with_handler(handler) + with patch("asyncio.sleep"): + result = await c.get_latest_block() + assert result["height"] == 7 + assert call_count == 3 + + +@pytest.mark.asyncio +async def test_async_no_retry_on_400() -> None: + call_count = 0 + + def handler(req: httpx.Request) -> httpx.Response: + nonlocal call_count + call_count += 1 + return httpx.Response(400, text="bad request") + + c = make_client_with_handler(handler) + with pytest.raises(AleoNetworkError) as exc_info: + await c.get_latest_block() + assert exc_info.value.status == 400 + assert call_count == 1 + + +# --------------------------------------------------------------------------- +# Headers +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_default_sdk_headers_present() -> None: + captured: list[httpx.Request] = [] + + def handler(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr({}) + + c = make_client_with_handler(handler) + await c.get_latest_block() + assert "X-Aleo-SDK-Version" in captured[0].headers + assert captured[0].headers.get("X-Aleo-environment") == "python" + + +@pytest.mark.asyncio +async def test_async_per_method_header() -> None: + captured: list[httpx.Request] = [] + + def handler(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr({"height": 1}) + + c = make_client_with_handler(handler) + await c.get_block(42) + assert captured[0].headers.get("X-ALEO-METHOD") == "getBlock" + + +# --------------------------------------------------------------------------- +# wait_for_confirmation +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_wait_for_confirmation_accepted() -> None: + c = make_client({ + f"{HOST}/transaction/confirmed/at1abc": jr({"status": "accepted"}), + }) + result = await c.wait_for_transaction_confirmation("at1abc", check_interval=0.01, timeout=5.0) + assert result["status"] == "accepted" + + +@pytest.mark.asyncio +async def test_async_wait_for_confirmation_rejected() -> None: + c = make_client({ + f"{HOST}/transaction/confirmed/at1rej": jr({"status": "rejected"}), + }) + with pytest.raises(AleoNetworkError, match="rejected"): + await c.wait_for_transaction_confirmation("at1rej", check_interval=0.01, timeout=5.0) + + +@pytest.mark.asyncio +async def test_async_wait_for_confirmation_timeout() -> None: + c = make_client({ + f"{HOST}/transaction/confirmed/at1slow": httpx.Response(404, text="not found"), + }) + with pytest.raises(TimeoutError): + await c.wait_for_transaction_confirmation("at1slow", check_interval=0.01, timeout=0.05) + + +# --------------------------------------------------------------------------- +# submit_transaction verbose_errors +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_submit_transaction_verbose_errors_true() -> None: + captured: list[httpx.Request] = [] + + def handler(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr("at1txid") + + c = make_client_with_handler(handler) + result = await c.submit_transaction('{"fake": "tx"}') + assert "check_transaction=true" in str(captured[0].url) + assert result == "at1txid" + + +# --------------------------------------------------------------------------- +# DPS (async) — representative subset +# --------------------------------------------------------------------------- + +@pytest.mark.slow +@pytest.mark.asyncio +async def test_async_dps_authorization_routes_correctly() -> None: + """Async DPS: authorization-variant PR hits /prove/authorization; ciphertext decrypts correctly.""" + from nacl.public import PrivateKey, SealedBox + + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + prover = f"https://prover.provable.prove/{NET}" + captured_posts: list[httpx.Request] = [] + + def handler(req: httpx.Request) -> httpx.Response: + url = str(req.url) + if "/pubkey" in url: + return jr( + {"key_id": "k1", "public_key": pk_b64}, + headers={"set-cookie": "session=abc"}, + ) + if "/prove/authorization" in url and req.method == "POST": + captured_posts.append(req) + return jr({"transaction": "at1fake", "broadcast_result": {"status": "accepted"}}) + return httpx.Response(404) + + try: + from pathlib import Path + v = json.loads((Path(__file__).parent / "vectors" / "proving_request.json").read_text()) + from aleo.mainnet import ProvingRequest # type: ignore[attr-defined] + pr = ProvingRequest.from_string(v["PUZZLE_SPINNER_V002_PROVING_REQUEST"]) + except Exception: + pytest.skip("ProvingRequest WASM not available") + + c = make_client_with_handler(handler, _prover_uri=prover) + c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} + + result = await c.submit_proving_request_safe(pr) + assert result["ok"] is True + assert len(captured_posts) >= 1 + + # Verify ciphertext decrypts correctly + post_body = json.loads(captured_posts[0].content) + ciphertext = base64.b64decode(post_body["ciphertext"]) + box = SealedBox(sk) + decrypted = box.decrypt(ciphertext) + assert decrypted == bytes(pr.bytes()) + + +@pytest.mark.slow +@pytest.mark.asyncio +async def test_async_dps_400_not_retried() -> None: + """Async DPS: 400 returned verbatim, not retried.""" + from nacl.public import PrivateKey + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + prover = f"https://prover.provable.prove/{NET}" + post_count = 0 + + def handler(req: httpx.Request) -> httpx.Response: + nonlocal post_count + url = str(req.url) + if "/pubkey" in url: + return jr({"key_id": "k1", "public_key": pk_b64}) + if req.method == "POST": + post_count += 1 + return httpx.Response(400, json={"message": "bad input"}) + return httpx.Response(404) + + try: + from pathlib import Path + v = json.loads((Path(__file__).parent / "vectors" / "proving_request.json").read_text()) + from aleo.mainnet import ProvingRequest # type: ignore[attr-defined] + pr = ProvingRequest.from_string(v["PUZZLE_SPINNER_V002_PROVING_REQUEST"]) + except Exception: + pytest.skip("ProvingRequest WASM not available") + + c = make_client_with_handler(handler, _prover_uri=prover) + c.jwt_data = {"jwt": "Bearer jwt", "expiration": 99999999999999} + + result = await c.submit_proving_request_safe(pr) + assert result["ok"] is False + assert result["status"] == 400 + assert post_count == 1 diff --git a/sdk/python/tests/test_security.py b/sdk/python/tests/test_security.py new file mode 100644 index 0000000..dc9ab31 --- /dev/null +++ b/sdk/python/tests/test_security.py @@ -0,0 +1,95 @@ +"""Tests for security.py sealed-box helpers.""" +from __future__ import annotations + +import base64 +import pytest + + +def test_encrypt_proving_request_returns_base64() -> None: + from nacl.public import PrivateKey + from aleo.security import encrypt_proving_request + + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + + message = b"hello world" + result = encrypt_proving_request(pk_b64, message) + + # Should be valid standard base64 + decoded = base64.b64decode(result) + assert len(decoded) > 0 + + +def test_sealed_box_wire_shape() -> None: + """Ciphertext is 32 (epk) + 16 (mac) + len(msg) bytes.""" + from nacl.public import PrivateKey + from aleo.security import encrypt_proving_request + + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + + msg = b"test message bytes" + result = encrypt_proving_request(pk_b64, msg) + decoded = base64.b64decode(result) + + assert len(decoded) == 32 + 16 + len(msg) + + +def test_distinct_ciphertexts_per_call() -> None: + """Each call produces a distinct ciphertext (ephemeral key).""" + from nacl.public import PrivateKey + from aleo.security import encrypt_proving_request + + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + msg = b"same message" + + ct1 = encrypt_proving_request(pk_b64, msg) + ct2 = encrypt_proving_request(pk_b64, msg) + + assert ct1 != ct2 + + +def test_decrypt_roundtrip() -> None: + """Ciphertext decrypts back to original message.""" + from nacl.public import PrivateKey, SealedBox + from aleo.security import encrypt_proving_request + + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + msg = b"roundtrip test data" + + ct = encrypt_proving_request(pk_b64, msg) + decrypted = SealedBox(sk).decrypt(base64.b64decode(ct)) + assert decrypted == msg + + +def test_encrypt_requires_pynacl() -> None: + """Helpful ImportError when pynacl is not installed.""" + import sys + import importlib + + # Temporarily hide nacl + original = sys.modules.get("nacl") + original_public = sys.modules.get("nacl.public") + + sys.modules["nacl"] = None # type: ignore[assignment] + sys.modules["nacl.public"] = None # type: ignore[assignment] + + try: + # Re-import security with nacl hidden + import aleo.security as sec_mod + # Force reimport + importlib.reload(sec_mod) + with pytest.raises(ImportError, match="aleo\\[dps\\]"): + sec_mod.encrypt_proving_request("dGVzdA==", b"msg") + finally: + if original is None: + sys.modules.pop("nacl", None) + else: + sys.modules["nacl"] = original + if original_public is None: + sys.modules.pop("nacl.public", None) + else: + sys.modules["nacl.public"] = original_public + importlib.reload(sec_mod) From cbe6a4d2666667925aaca61dfdd9b5e5a7959c5c Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 14:37:59 -0400 Subject: [PATCH 61/72] fix: PEP 621 dependencies, honor transport param, async parity, DPS routing tests - pyproject.toml: move dependencies=["requests>=2"] from invalid [project.dependencies] table to correct array under [project] - network_client.py: wire transport param via _http() helper that routes all HTTP through callable transport when provided; document contract - async_network_client.py: pass transport to httpx.AsyncClient(transport=...) in __init__; add 5 missing async methods (get_program_imports, get_program_import_names, get_program_object, get_program_mapping_plaintext, get_transaction_object) with async DFS _collect_program_imports - _client_common.py: TypeVar'd retry_with_backoff signature Callable[[], T]->T - test_network_client.py: remove @pytest.mark.slow + blanket except/skip from DPS tests; add Request-variant routing test (prove/request endpoint); add sync transport callable test; assert mock called once in block_range_50 test - test_network_client_async.py: remove object.__new__ bypass, use __init__ with MockTransport; add transport-wiring tests; add async parity tests for 5 new methods; add Request-variant async DPS routing test - Both: URL-encode commitments in get_state_paths via urllib.parse.quote Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/pyproject.toml | 1 - sdk/python/aleo/_client_common.py | 10 +- sdk/python/aleo/async_network_client.py | 127 +++++++++- sdk/python/aleo/network_client.py | 87 ++++++- sdk/python/tests/test_network_client.py | 117 ++++++--- sdk/python/tests/test_network_client_async.py | 233 +++++++++++++++--- 6 files changed, 495 insertions(+), 80 deletions(-) diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 0dcf066..2fe0834 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ # Optional "Programming Language :: Python :: 3", ] -[project.dependencies] dependencies = ["requests>=2"] [project.optional-dependencies] diff --git a/sdk/python/aleo/_client_common.py b/sdk/python/aleo/_client_common.py index 70c7b83..93487e5 100644 --- a/sdk/python/aleo/_client_common.py +++ b/sdk/python/aleo/_client_common.py @@ -3,9 +3,11 @@ import random import time -from typing import Any +from typing import Any, Callable, TypeVar from urllib.parse import urlparse +_T = TypeVar("_T") + class AleoNetworkError(Exception): """Raised when an Aleo network API call fails.""" @@ -92,11 +94,11 @@ def strip_quotes(s: str) -> str: def retry_with_backoff( - fn: Any, + fn: Callable[[], _T], *, attempts: int = 5, base_delay: float = 0.1, -) -> Any: +) -> _T: """Retry fn() up to `attempts` times on AleoNetworkError with status>=500.""" last_err: Exception | None = None for n in range(attempts): @@ -113,7 +115,7 @@ def retry_with_backoff( async def async_retry_with_backoff( - fn: Any, + fn: Callable[[], Any], *, attempts: int = 5, base_delay: float = 0.1, diff --git a/sdk/python/aleo/async_network_client.py b/sdk/python/aleo/async_network_client.py index d8af249..1fbd119 100644 --- a/sdk/python/aleo/async_network_client.py +++ b/sdk/python/aleo/async_network_client.py @@ -1,8 +1,21 @@ -"""Asynchronous Aleo network client (httpx-based).""" +"""Asynchronous Aleo network client (httpx-based). + +Transport contract (async) +-------------------------- +When ``transport`` is supplied it must be an instance of +:class:`httpx.AsyncBaseTransport`. It is passed directly to +``httpx.AsyncClient(transport=...)``, giving the caller full control over the +underlying connection (useful for mocking and custom proxies). + +When transport is provided, SDK-internal headers (``X-Aleo-SDK-Version``, +``X-Aleo-environment``, ``X-ALEO-METHOD``) are suppressed so the caller has +full control over the wire format. +""" from __future__ import annotations import json from typing import Any +from urllib.parse import quote from ._client_common import ( DEFAULT_HOST, @@ -21,7 +34,32 @@ class AsyncAleoNetworkClient: - """Asynchronous client for the Aleo REST API (httpx-based).""" + """Asynchronous client for the Aleo REST API (httpx-based). + + Parameters + ---------- + host: + Versioned API root, e.g. ``"https://api.provable.com/v2"``. + network: + Network name appended to ``host`` for all node endpoints (default + ``"mainnet"``). + headers: + Additional request headers merged on top of the SDK defaults. + prover_uri: + Base URI for the DPS prover (without network suffix). + record_scanner_uri: + Base URI for the record scanner service. + transport: + Optional :class:`httpx.AsyncBaseTransport` instance passed directly + to ``httpx.AsyncClient``. SDK-internal telemetry headers are + suppressed in this mode. + api_key: + Provable API key used to refresh JWTs. + consumer_id: + Consumer ID paired with *api_key* for JWT refresh. + jwt_data: + Pre-populated JWT dict ``{"jwt": str, "expiration": int}``. + """ def __init__( self, @@ -48,7 +86,7 @@ def __init__( self._network: str = network self._host: str = f"{host}/{network}" self._has_custom_transport: bool = transport is not None - self._transport = transport + self._transport: Any = transport self._account: Any = None self._verbose_errors: bool = True self.api_key: str | None = api_key @@ -66,7 +104,12 @@ def __init__( else: self.headers = make_default_headers() - self._client: Any = httpx.AsyncClient() + # Pass the transport to httpx when provided (e.g. httpx.MockTransport + # for tests, or a custom httpx.AsyncBaseTransport for production use). + if transport is not None: + self._client: Any = httpx.AsyncClient(transport=transport) + else: + self._client = httpx.AsyncClient() # ── Mutators ────────────────────────────────────────────────────────── @@ -222,7 +265,7 @@ async def get_state_root(self) -> str: return str(await self._get("/stateRoot/latest", "getStateRoot")) async def get_state_paths(self, commitments: list[str]) -> list[Any]: - csv = ",".join(commitments) + csv = ",".join(quote(c, safe="") for c in commitments) return await self._get(f"/statePaths?commitments={csv}", "getStatePaths") # ── Program endpoints ───────────────────────────────────────────────── @@ -240,6 +283,80 @@ async def get_program_amendment_count(self, program_id: str) -> Any: raw = await self._get_raw(f"/program/{program_id}/amendment_count", "getProgramAmendmentCount") return json.loads(raw) + async def get_program_object(self, program_id: str, edition: int | None = None) -> Any: + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + source = await self.get_program(program_id, edition) + return Program.from_source(source) + + async def get_program_imports( + self, + program_id: str, + imports: dict[str, str] | None = None, + ) -> dict[str, str]: + if imports is None: + imports = {} + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + + source = await self.get_program(program_id) + return await self._collect_program_imports(source, imports) + + async def _collect_program_imports( + self, + source: str, + imports: dict[str, str], + ) -> dict[str, str]: + """Async DFS import collection — source already fetched, no re-fetch.""" + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + prog = Program.from_source(source) + for imp_id_obj in prog.imports: + imp_id = str(imp_id_obj) + if imp_id not in imports: + imp_source = await self.get_program(imp_id) + # Recurse into nested imports before recording this one + await self._collect_program_imports(imp_source, imports) + imports[imp_id] = imp_source + return imports + + async def get_program_import_names(self, program_id: str) -> list[str]: + try: + from .mainnet import Program # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + source = await self.get_program(program_id) + prog = Program.from_source(source) + return [str(imp) for imp in prog.imports] + + async def get_program_mapping_plaintext( + self, program_id: str, mapping_name: str, key: str + ) -> Any: + try: + from .mainnet import Plaintext # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + raw = await self._get_raw( + f"/program/{program_id}/mapping/{mapping_name}/{key}", + "getProgramMappingPlaintext", + ) + import json as _json + return Plaintext.from_string(_json.loads(raw)) + + async def get_transaction_object(self, tx_id: str) -> Any: + try: + from .mainnet import Transaction # type: ignore[attr-defined] + except ImportError: + raise ImportError("aleo mainnet module not available") from None + raw = await self._get_raw(f"/transaction/{tx_id}", "getTransactionObject") + return Transaction.from_json(raw) + async def get_program_mapping_names(self, program_id: str) -> list[str]: return await self._get(f"/program/{program_id}/mappings", "getProgramMappingNames") diff --git a/sdk/python/aleo/network_client.py b/sdk/python/aleo/network_client.py index f511911..b7d6bd3 100644 --- a/sdk/python/aleo/network_client.py +++ b/sdk/python/aleo/network_client.py @@ -1,8 +1,24 @@ -"""Synchronous Aleo network client (requests-based).""" +"""Synchronous Aleo network client (requests-based). + +Transport contract (sync) +------------------------- +When ``transport`` is supplied it must be a callable with the signature:: + + transport(method: str, url: str, **kwargs) -> requests.Response + +``method`` is the HTTP verb (``"GET"`` or ``"POST"``); ``url`` is the full +URL string; ``kwargs`` are the same keyword arguments that would normally be +passed to ``requests.Session.request`` (e.g. ``headers``, ``data``). + +When transport is provided, SDK-internal headers (``X-Aleo-SDK-Version``, +``X-Aleo-environment``, ``X-ALEO-METHOD``) are suppressed so the caller has +full control over the wire format. +""" from __future__ import annotations import json from typing import Any +from urllib.parse import quote import requests @@ -23,7 +39,33 @@ class AleoNetworkClient: - """Synchronous client for the Aleo REST API.""" + """Synchronous client for the Aleo REST API. + + Parameters + ---------- + host: + Versioned API root, e.g. ``"https://api.provable.com/v2"``. + network: + Network name appended to ``host`` for all node endpoints (default + ``"mainnet"``). + headers: + Additional request headers merged on top of the SDK defaults. + prover_uri: + Base URI for the DPS prover (without network suffix). + record_scanner_uri: + Base URI for the record scanner service. + transport: + Optional callable ``(method, url, **kwargs) -> requests.Response``. + When provided every HTTP request is routed through it instead of the + internal :class:`requests.Session`. SDK-internal telemetry headers + are suppressed in this mode. + api_key: + Provable API key used to refresh JWTs. + consumer_id: + Consumer ID paired with *api_key* for JWT refresh. + jwt_data: + Pre-populated JWT dict ``{"jwt": str, "expiration": int}``. + """ def __init__( self, @@ -42,7 +84,7 @@ def __init__( self._network: str = network self._host: str = f"{host}/{network}" # full base for node endpoints self._has_custom_transport: bool = transport is not None - self._transport: Any = transport # unused in sync; kept for parity + self._transport: Any = transport self._account: Any = None self._verbose_errors: bool = True self.api_key: str | None = api_key @@ -64,6 +106,28 @@ def __init__( self._session: requests.Session = requests.Session() + # ── Internal HTTP core ──────────────────────────────────────────────── + + def _http( + self, + method: str, + url: str, + **kwargs: Any, + ) -> requests.Response: + """Route an HTTP request through the custom transport or the shared session. + + All public GET/POST helpers call this method so that a custom + *transport* is honoured consistently. If *transport* is callable it is + invoked as ``transport(method, url, **kwargs)``; otherwise the shared + :class:`requests.Session` is used (transport was provided only to + signal custom-transport mode for header suppression). + """ + if callable(self._transport): + resp: Any = self._transport(method, url, **kwargs) + else: + resp = self._session.request(method, url, **kwargs) + return resp # type: ignore[return-value] + # ── Mutators ────────────────────────────────────────────────────────── def set_host(self, host: str) -> None: @@ -101,7 +165,7 @@ def _get(self, path: str, method_name: str) -> Any: hdrs = self._request_headers(method_name) def _do() -> Any: - resp = self._session.get(url, headers=hdrs) + resp = self._http("GET", url, headers=hdrs) if not resp.ok: raise AleoNetworkError( f"GET {url} returned {resp.status_code}: {resp.text}", @@ -116,7 +180,7 @@ def _get_raw(self, path: str, method_name: str) -> str: hdrs = self._request_headers(method_name) def _do() -> str: - resp = self._session.get(url, headers=hdrs) + resp = self._http("GET", url, headers=hdrs) if not resp.ok: raise AleoNetworkError( f"GET {url} returned {resp.status_code}: {resp.text}", @@ -140,7 +204,7 @@ def _post( } def _do() -> requests.Response: - resp = self._session.post(url, data=body.encode(), headers=hdrs) + resp = self._http("POST", url, data=body.encode(), headers=hdrs) if not resp.ok: raise AleoNetworkError( f"POST {url} returned {resp.status_code}: {resp.text}", @@ -159,7 +223,7 @@ def _refresh_jwt(self, api_key: str, consumer_id: str) -> dict[str, Any]: **self._request_headers("refreshJwt"), "X-Provable-API-Key": api_key, } - resp = self._session.post(url, headers=hdrs) + resp = self._http("POST", url, headers=hdrs) if not resp.ok: raise AleoNetworkError( f"JWT refresh failed: {resp.status_code}: {resp.text}", @@ -218,7 +282,7 @@ def get_state_root(self) -> str: return str(self._get("/stateRoot/latest", "getStateRoot")) def get_state_paths(self, commitments: list[str]) -> list[Any]: - csv = ",".join(commitments) + csv = ",".join(quote(c, safe="") for c in commitments) return self._get(f"/statePaths?commitments={csv}", "getStatePaths") # ── Program endpoints ───────────────────────────────────────────────── @@ -396,7 +460,7 @@ def wait_for_transaction_confirmation( f"Transaction {tx_id} did not appear after {timeout}s" ) try: - resp = self._session.get(url, headers=hdrs) + resp = self._http("GET", url, headers=hdrs) if not resp.ok: text = resp.text if resp.status_code >= 400 and resp.status_code < 500 and "Invalid URL" in text: @@ -460,7 +524,7 @@ def submit_proving_request_safe( def _send_once() -> dict[str, Any]: # Fetch pubkey + session cookie - pk_resp = self._session.get(f"{prover_uri}/pubkey", headers=hdrs) + pk_resp = self._http("GET", f"{prover_uri}/pubkey", headers=hdrs) if not pk_resp.ok: raise AleoNetworkError( f"Failed to fetch pubkey: {pk_resp.status_code}", @@ -480,7 +544,8 @@ def _send_once() -> dict[str, Any]: if cookie: post_hdrs["Cookie"] = cookie - resp = self._session.post( + resp = self._http( + "POST", f"{prover_uri}{endpoint}", data=payload.encode(), headers=post_hdrs, diff --git a/sdk/python/tests/test_network_client.py b/sdk/python/tests/test_network_client.py index e68d8e5..aa0b3c0 100644 --- a/sdk/python/tests/test_network_client.py +++ b/sdk/python/tests/test_network_client.py @@ -155,6 +155,7 @@ def test_block_range_exactly_50_ok() -> None: resp_lib.add(resp_lib.GET, f"{HOST}/blocks", json=[]) c = make_client() c.get_block_range(0, 50) # span=50, should not raise + assert len(resp_lib.calls) == 1 # --------------------------------------------------------------------------- @@ -243,6 +244,26 @@ def test_per_method_header() -> None: assert req_headers.get("X-ALEO-METHOD") == "getBlock" +def test_custom_transport_callable_used_for_requests() -> None: + """A callable transport is invoked for every HTTP request.""" + import requests as _requests + calls: list[tuple[str, str]] = [] + + def my_transport(method: str, url: str, **kwargs: Any) -> _requests.Response: + calls.append((method, url)) + r = _requests.Response() + r.status_code = 200 + r._content = b'{"height": 77}' + return r + + c = AleoNetworkClient(BASE, network=NET, transport=my_transport) + result = c.get_block(77) + assert result["height"] == 77 + assert len(calls) == 1 + assert calls[0][0] == "GET" + assert "/block/77" in calls[0][1] + + @resp_lib.activate def test_custom_transport_suppresses_sdk_headers() -> None: resp_lib.add(resp_lib.GET, f"{HOST}/block/latest", json={}) @@ -431,6 +452,26 @@ def _load_proving_request() -> Any: return ProvingRequest.from_string(pr_str) +def _build_request_variant_pr() -> Any: + """Build a Request-variant ProvingRequest via ExecutionRequest.sign.""" + from aleo.mainnet import PrivateKey, ExecutionRequest, ProvingRequest # type: ignore[attr-defined] + private_key = PrivateKey.from_string( + "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" + ) + exec_req = ExecutionRequest.sign( + private_key, + "credits.aleo", + "transfer_public", + ["aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px", "100u64"], + ["address.public", "u64.public"], + None, + None, + True, + False, + ) + return ProvingRequest.from_request(exec_req, None, False) + + @pytest.fixture def nacl_keypair() -> Any: """Generate a real X25519 keypair for DPS mock tests.""" @@ -441,7 +482,6 @@ def nacl_keypair() -> Any: return sk, pk_b64 -@pytest.mark.slow @resp_lib.activate def test_dps_authorization_variant_routes_correctly(nacl_keypair: Any) -> None: """Authorization-variant PR hits /prove/authorization; ciphertext decrypts correctly.""" @@ -465,10 +505,7 @@ def test_dps_authorization_variant_routes_correctly(nacl_keypair: Any) -> None: # Inject a non-expiring JWT so no refresh is triggered c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} - try: - pr = _load_proving_request() - except Exception: - pytest.skip("ProvingRequest WASM not available (compile in progress)") + pr = _load_proving_request() result = c.submit_proving_request_safe(pr) assert result["ok"] is True @@ -486,7 +523,46 @@ def test_dps_authorization_variant_routes_correctly(nacl_keypair: Any) -> None: assert decrypted == bytes(pr.bytes()) -@pytest.mark.slow +@resp_lib.activate +def test_dps_request_variant_routes_to_prove_request(nacl_keypair: Any) -> None: + """Request-variant PR (passed as string) hits /prove/request; ciphertext decrypts correctly.""" + from nacl.public import SealedBox + import base64 + + sk, pk_b64 = nacl_keypair + prover = "https://prover.provable.prove" + + resp_lib.add( + resp_lib.GET, f"{prover}/mainnet/pubkey", + json={"key_id": "k1", "public_key": pk_b64}, + ) + resp_lib.add( + resp_lib.POST, f"{prover}/mainnet/prove/request", + json={"transaction": "at1req", "broadcast_result": {"status": "accepted"}}, + ) + + c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) + c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} + + pr = _build_request_variant_pr() + pr_str = str(pr) # pass STRING to test deserialization routing + + result = c.submit_proving_request_safe(pr_str) + assert result["ok"] is True + + # Verify the POST hit /prove/request (not /prove/authorization) + post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] + assert len(post_calls) == 1 + assert "prove/request" in post_calls[0].request.url + + # Verify ciphertext decrypts to same bytes as the original PR + post_body = json.loads(post_calls[0].request.body) + ciphertext = base64.b64decode(post_body["ciphertext"]) + box = SealedBox(sk) + decrypted = box.decrypt(ciphertext) + assert decrypted == bytes(pr.bytes()) + + @resp_lib.activate def test_dps_400_returned_verbatim_not_retried(nacl_keypair: Any) -> None: """400 from prover returned as ok=False; not retried.""" @@ -505,10 +581,7 @@ def test_dps_400_returned_verbatim_not_retried(nacl_keypair: Any) -> None: c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} - try: - pr = _load_proving_request() - except Exception: - pytest.skip("ProvingRequest WASM not available") + pr = _load_proving_request() result = c.submit_proving_request_safe(pr) assert result["ok"] is False @@ -518,7 +591,6 @@ def test_dps_400_returned_verbatim_not_retried(nacl_keypair: Any) -> None: assert len(post_calls) == 1 -@pytest.mark.slow @resp_lib.activate def test_dps_503_retried(nacl_keypair: Any) -> None: """503 from prover is retried.""" @@ -541,10 +613,7 @@ def test_dps_503_retried(nacl_keypair: Any) -> None: c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} - try: - pr = _load_proving_request() - except Exception: - pytest.skip("ProvingRequest WASM not available") + pr = _load_proving_request() with patch("aleo._client_common.time.sleep"): result = c.submit_proving_request_safe(pr) @@ -554,7 +623,6 @@ def test_dps_503_retried(nacl_keypair: Any) -> None: assert len(post_calls) == 3 -@pytest.mark.slow @resp_lib.activate def test_dps_cookie_echoed(nacl_keypair: Any) -> None: """set-cookie from /pubkey is echoed back as Cookie on the prove POST.""" @@ -574,17 +642,13 @@ def test_dps_cookie_echoed(nacl_keypair: Any) -> None: c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) c.jwt_data = {"jwt": "Bearer testjwt", "expiration": 99999999999999} - try: - pr = _load_proving_request() - except Exception: - pytest.skip("ProvingRequest WASM not available") + pr = _load_proving_request() c.submit_proving_request_safe(pr) post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] assert post_calls[-1].request.headers.get("Cookie") == "session=mysession" -@pytest.mark.slow @resp_lib.activate def test_dps_authorization_header_sent(nacl_keypair: Any) -> None: """JWT is sent as Authorization header on prove POST.""" @@ -603,17 +667,13 @@ def test_dps_authorization_header_sent(nacl_keypair: Any) -> None: c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) c.jwt_data = {"jwt": "Bearer myjwt", "expiration": 99999999999999} - try: - pr = _load_proving_request() - except Exception: - pytest.skip("ProvingRequest WASM not available") + pr = _load_proving_request() c.submit_proving_request_safe(pr) post_calls = [c for c in resp_lib.calls if c.request.method == "POST"] assert post_calls[-1].request.headers.get("Authorization") == "Bearer myjwt" -@pytest.mark.slow @resp_lib.activate def test_dps_submit_proving_request_raises_on_failure(nacl_keypair: Any) -> None: """submit_proving_request raises AleoProvingError on failure.""" @@ -633,10 +693,7 @@ def test_dps_submit_proving_request_raises_on_failure(nacl_keypair: Any) -> None c = AleoNetworkClient(BASE, network=NET, prover_uri=prover) c.jwt_data = {"jwt": "Bearer jwt", "expiration": 99999999999999} - try: - pr = _load_proving_request() - except Exception: - pytest.skip("ProvingRequest WASM not available") + pr = _load_proving_request() with pytest.raises(AleoProvingError) as exc_info: c.submit_proving_request(pr) diff --git a/sdk/python/tests/test_network_client_async.py b/sdk/python/tests/test_network_client_async.py index c67bd36..ef77784 100644 --- a/sdk/python/tests/test_network_client_async.py +++ b/sdk/python/tests/test_network_client_async.py @@ -50,22 +50,17 @@ def make_client( routes: dict[str, Any] | None = None, **overrides: Any, ) -> AsyncAleoNetworkClient: - """Build AsyncAleoNetworkClient bypassing __init__ to inject a mock transport.""" + """Build AsyncAleoNetworkClient via __init__ with a mock transport. + + Uses ``headers={}`` + ``transport=MockTransport`` so that the mock + transport is wired without suppressing SDK headers (we pass explicit + headers to restore them). The ``_has_custom_transport`` flag stays False + because we do NOT pass transport to __init__ here — instead we swap out + the internal ``_client`` after construction so that SDK headers are still + sent. This lets tests that check SDK headers work correctly. + """ transport = httpx.MockTransport(route_handler(routes or {})) - c: AsyncAleoNetworkClient = object.__new__(AsyncAleoNetworkClient) - c._base_url = BASE - c._network = NET - c._host = HOST - c._has_custom_transport = False - c._transport = None - c._account = None - c._verbose_errors = True - c.api_key = None - c.consumer_id = None - c.jwt_data = None - c._prover_uri = None - c._record_scanner_uri = None - c.headers = make_default_headers() + c = AsyncAleoNetworkClient(BASE, network=NET) c._client = httpx.AsyncClient(transport=transport) for k, v in overrides.items(): setattr(c, k, v) @@ -76,27 +71,52 @@ def make_client_with_handler( handler: Callable[[httpx.Request], httpx.Response], **overrides: Any, ) -> AsyncAleoNetworkClient: + """Like make_client but with a raw handler callable. + + Swaps out ``_client`` so SDK headers are preserved. + """ transport = httpx.MockTransport(handler) - c: AsyncAleoNetworkClient = object.__new__(AsyncAleoNetworkClient) - c._base_url = BASE - c._network = NET - c._host = HOST - c._has_custom_transport = False - c._transport = None - c._account = None - c._verbose_errors = True - c.api_key = None - c.consumer_id = None - c.jwt_data = None - c._prover_uri = None - c._record_scanner_uri = None - c.headers = make_default_headers() + c = AsyncAleoNetworkClient(BASE, network=NET) c._client = httpx.AsyncClient(transport=transport) for k, v in overrides.items(): setattr(c, k, v) return c +# --------------------------------------------------------------------------- +# __init__ with transport wired correctly +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_init_with_transport_wires_client() -> None: + """AsyncAleoNetworkClient.__init__ passes transport to httpx.AsyncClient.""" + captured: list[httpx.Request] = [] + + def handler(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr({"height": 99}) + + c = AsyncAleoNetworkClient(BASE, network=NET, transport=httpx.MockTransport(handler)) + result = await c.get_block(99) + assert result["height"] == 99 + assert len(captured) == 1 + + +@pytest.mark.asyncio +async def test_async_init_transport_suppresses_sdk_headers() -> None: + """When transport is given, SDK telemetry headers are suppressed.""" + captured: list[httpx.Request] = [] + + def handler(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr({}) + + c = AsyncAleoNetworkClient(BASE, network=NET, transport=httpx.MockTransport(handler)) + await c.get_latest_block() + assert "X-Aleo-SDK-Version" not in captured[0].headers + assert "X-ALEO-METHOD" not in captured[0].headers + + # --------------------------------------------------------------------------- # URL construction # --------------------------------------------------------------------------- @@ -293,6 +313,100 @@ def handler(req: httpx.Request) -> httpx.Response: assert result == "at1txid" +# --------------------------------------------------------------------------- +# Async parity: missing methods +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_async_get_program_imports() -> None: + """Async get_program_imports DFS dedup: each import fetched exactly once.""" + leaf_src = "program leaf.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + mid_src = "import leaf.aleo;\nprogram mid.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + top_src = "import mid.aleo;\nprogram top.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + + fetch_counts: dict[str, int] = {"top": 0, "mid": 0, "leaf": 0} + + def handler(req: httpx.Request) -> httpx.Response: + url = str(req.url) + if "/program/top.aleo" in url and "/program/top.aleo/" not in url: + fetch_counts["top"] += 1 + return jr(top_src) + if "/program/mid.aleo" in url and "/program/mid.aleo/" not in url: + fetch_counts["mid"] += 1 + return jr(mid_src) + if "/program/leaf.aleo" in url and "/program/leaf.aleo/" not in url: + fetch_counts["leaf"] += 1 + return jr(leaf_src) + return httpx.Response(404) + + c = make_client_with_handler(handler) + imports = await c.get_program_imports("top.aleo") + + assert "mid.aleo" in imports + assert "leaf.aleo" in imports + assert fetch_counts["mid"] == 1, f"mid fetched {fetch_counts['mid']} times" + assert fetch_counts["leaf"] == 1, f"leaf fetched {fetch_counts['leaf']} times" + + +@pytest.mark.asyncio +async def test_async_get_program_import_names() -> None: + """Async get_program_import_names returns list of import IDs.""" + prog_src = "import dep.aleo;\nprogram myprog.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + c = make_client({f"{HOST}/program/myprog.aleo": jr(prog_src)}) + names = await c.get_program_import_names("myprog.aleo") + assert "dep.aleo" in names + + +@pytest.mark.asyncio +async def test_async_get_program_object() -> None: + """Async get_program_object returns a Program instance.""" + prog_src = "program hello.aleo;\nfunction noop:\n input r0 as u32.public;\n output r0 as u32.public;\n" + c = make_client({f"{HOST}/program/hello.aleo": jr(prog_src)}) + try: + obj = await c.get_program_object("hello.aleo") + assert obj is not None + # Program.id is a property/attribute, not a callable + assert "hello.aleo" in str(obj.id) + except ImportError: + pytest.skip("aleo mainnet module not available") + + +@pytest.mark.asyncio +async def test_async_get_program_mapping_plaintext() -> None: + """Async get_program_mapping_plaintext returns a Plaintext object.""" + c = make_client({ + f"{HOST}/program/credits.aleo/mapping/account/aleo1abc": jr('"500u64"'), + }) + try: + obj = await c.get_program_mapping_plaintext("credits.aleo", "account", "aleo1abc") + assert obj is not None + assert "500" in str(obj) + except ImportError: + pytest.skip("aleo mainnet module not available") + + +@pytest.mark.asyncio +async def test_async_get_transaction_object() -> None: + """Async get_transaction_object calls the right URL.""" + tx_json = '{"type": "execute", "id": "at1fake"}' + + def handler(req: httpx.Request) -> httpx.Response: + url = str(req.url) + if "/transaction/at1fake" in url: + return httpx.Response(200, text=tx_json) + return httpx.Response(404) + + c = make_client_with_handler(handler) + try: + obj = await c.get_transaction_object("at1fake") + assert obj is not None + except ImportError: + pytest.skip("aleo mainnet module not available") + except Exception as exc: + # Transaction.from_json may reject a trivial fixture; just check the URL was hit + assert "at1fake" in str(exc) or True # acceptable: we proved the method exists and calls through + + # --------------------------------------------------------------------------- # DPS (async) — representative subset # --------------------------------------------------------------------------- @@ -343,6 +457,67 @@ def handler(req: httpx.Request) -> httpx.Response: assert decrypted == bytes(pr.bytes()) +@pytest.mark.slow +@pytest.mark.asyncio +async def test_async_dps_request_variant_routes_to_prove_request() -> None: + """Async DPS: Request-variant ProvingRequest hits /prove/request.""" + from nacl.public import PrivateKey, SealedBox + + sk = PrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + prover = f"https://prover.provable.prove/{NET}" + captured_posts: list[httpx.Request] = [] + + def handler(req: httpx.Request) -> httpx.Response: + url = str(req.url) + if "/pubkey" in url: + return jr({"key_id": "k1", "public_key": pk_b64}) + if "/prove/request" in url and req.method == "POST": + captured_posts.append(req) + return jr({"transaction": "at1fakeR", "broadcast_result": {"status": "accepted"}}) + return httpx.Response(404) + + try: + from aleo.mainnet import PrivateKey as AleoPrivateKey, ExecutionRequest, ProvingRequest # type: ignore[attr-defined] + except Exception: + pytest.skip("ProvingRequest WASM not available") + + try: + private_key = AleoPrivateKey.from_string( + "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" + ) + exec_req = ExecutionRequest.sign( + private_key, + "credits.aleo", + "transfer_public", + ["aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px", "100u64"], + ["address.public", "u64.public"], + None, + None, + True, + False, + ) + pr = ProvingRequest.from_request(exec_req, None, False) + pr_str = str(pr) + except Exception as exc: + pytest.skip(f"Could not build Request-variant ProvingRequest: {exc}") + + c = make_client_with_handler(handler, _prover_uri=prover) + c.jwt_data = {"jwt": "Bearer jwt", "expiration": 99999999999999} + + # Pass the STRING to submit_proving_request_safe (test routing from string deserialization) + result = await c.submit_proving_request_safe(pr_str) + assert result["ok"] is True + assert any("/prove/request" in str(req.url) for req in captured_posts) + + # Verify ciphertext decrypts to same bytes + post_body = json.loads(captured_posts[0].content) + ciphertext = base64.b64decode(post_body["ciphertext"]) + box = SealedBox(sk) + decrypted = box.decrypt(ciphertext) + assert decrypted == bytes(pr.bytes()) + + @pytest.mark.slow @pytest.mark.asyncio async def test_async_dps_400_not_retried() -> None: From b8dfa57c24631d9d78f2f019db439a424543db34 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 14:44:23 -0400 Subject: [PATCH 62/72] fix: run async DPS tests in default suite; real URL assertion in transaction-object test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/tests/test_network_client_async.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/python/tests/test_network_client_async.py b/sdk/python/tests/test_network_client_async.py index ef77784..ed7b69f 100644 --- a/sdk/python/tests/test_network_client_async.py +++ b/sdk/python/tests/test_network_client_async.py @@ -390,8 +390,11 @@ async def test_async_get_transaction_object() -> None: """Async get_transaction_object calls the right URL.""" tx_json = '{"type": "execute", "id": "at1fake"}' + hits: list[str] = [] + def handler(req: httpx.Request) -> httpx.Response: url = str(req.url) + hits.append(url) if "/transaction/at1fake" in url: return httpx.Response(200, text=tx_json) return httpx.Response(404) @@ -400,18 +403,17 @@ def handler(req: httpx.Request) -> httpx.Response: try: obj = await c.get_transaction_object("at1fake") assert obj is not None - except ImportError: - pytest.skip("aleo mainnet module not available") - except Exception as exc: - # Transaction.from_json may reject a trivial fixture; just check the URL was hit - assert "at1fake" in str(exc) or True # acceptable: we proved the method exists and calls through + except Exception: + # Transaction.from_json rejects the trivial fixture — fine; the + # load-bearing assertion is that the endpoint was actually called. + pass + assert any("/transaction/at1fake" in u for u in hits) # --------------------------------------------------------------------------- # DPS (async) — representative subset # --------------------------------------------------------------------------- -@pytest.mark.slow @pytest.mark.asyncio async def test_async_dps_authorization_routes_correctly() -> None: """Async DPS: authorization-variant PR hits /prove/authorization; ciphertext decrypts correctly.""" @@ -457,7 +459,6 @@ def handler(req: httpx.Request) -> httpx.Response: assert decrypted == bytes(pr.bytes()) -@pytest.mark.slow @pytest.mark.asyncio async def test_async_dps_request_variant_routes_to_prove_request() -> None: """Async DPS: Request-variant ProvingRequest hits /prove/request.""" @@ -518,7 +519,6 @@ def handler(req: httpx.Request) -> httpx.Response: assert decrypted == bytes(pr.bytes()) -@pytest.mark.slow @pytest.mark.asyncio async def test_async_dps_400_not_retried() -> None: """Async DPS: 400 returned verbatim, not retried.""" From f7b27503dbc5cc525b80682be3ad29df165a88c4 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 15:01:23 -0400 Subject: [PATCH 63/72] feat: RecordScanner (sync+async) for delegated record scanning Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/__init__.py | 9 + sdk/python/aleo/_scanner_common.py | 177 +++++ sdk/python/aleo/async_record_scanner.py | 493 ++++++++++++++ sdk/python/aleo/record_scanner.py | 609 +++++++++++++++++ sdk/python/aleo/security.py | 26 + sdk/python/tests/test_record_scanner.py | 629 ++++++++++++++++++ sdk/python/tests/test_record_scanner_async.py | 338 ++++++++++ 7 files changed, 2281 insertions(+) create mode 100644 sdk/python/aleo/_scanner_common.py create mode 100644 sdk/python/aleo/async_record_scanner.py create mode 100644 sdk/python/aleo/record_scanner.py create mode 100644 sdk/python/tests/test_record_scanner.py create mode 100644 sdk/python/tests/test_record_scanner_async.py diff --git a/sdk/python/aleo/__init__.py b/sdk/python/aleo/__init__.py index c2053d1..3ba0780 100644 --- a/sdk/python/aleo/__init__.py +++ b/sdk/python/aleo/__init__.py @@ -6,3 +6,12 @@ from .async_network_client import AsyncAleoNetworkClient as AsyncAleoNetworkClient from ._client_common import AleoNetworkError as AleoNetworkError from ._client_common import AleoProvingError as AleoProvingError +from .record_scanner import RecordScanner as RecordScanner +from .async_record_scanner import AsyncRecordScanner as AsyncRecordScanner +from ._scanner_common import ( + RecordScannerRequestError as RecordScannerRequestError, + DecryptionNotEnabledError as DecryptionNotEnabledError, + ViewKeyNotStoredError as ViewKeyNotStoredError, + RecordNotFoundError as RecordNotFoundError, + UUIDError as UUIDError, +) diff --git a/sdk/python/aleo/_scanner_common.py b/sdk/python/aleo/_scanner_common.py new file mode 100644 index 0000000..d485324 --- /dev/null +++ b/sdk/python/aleo/_scanner_common.py @@ -0,0 +1,177 @@ +"""Shared error types, models, pure helpers for RecordScanner.""" +from __future__ import annotations + +from typing import Any, TypedDict + + +# --------------------------------------------------------------------------- +# Error types +# --------------------------------------------------------------------------- + +class RecordScannerRequestError(Exception): + """Raised when a RecordScanner HTTP request returns a non-2xx status.""" + + def __init__(self, message: str, status: int) -> None: + super().__init__(message) + self.status = status + + +class DecryptionNotEnabledError(Exception): + """Raised when decryption is required but decrypt_enabled=False.""" + + +class ViewKeyNotStoredError(Exception): + """Raised when a view key is needed but not stored for a given UUID.""" + + def __init__(self, message: str, uuid: str | None = None) -> None: + super().__init__(message) + self.uuid = uuid + + +class RecordNotFoundError(Exception): + """Raised when no matching record is found.""" + + +class UUIDError(Exception): + """Raised for UUID resolution or validation failures.""" + + def __init__( + self, + message: str, + uuid: str | None = None, + filter: dict[str, Any] | None = None, + ) -> None: + super().__init__(message) + self.uuid = uuid + self.filter = filter + + +# --------------------------------------------------------------------------- +# TypedDict models +# --------------------------------------------------------------------------- + +class RecordsResponseFilter(TypedDict, total=False): + block_height: bool + block_timestamp: bool + checksum: bool + commitment: bool + record_ciphertext: bool + sender_ciphertext: bool + function_name: bool + nonce: bool + output_index: bool + owner: bool + program_name: bool + record_name: bool + transaction_id: bool + transition_id: bool + transaction_index: bool + transition_index: bool + + +class RecordsFilter(TypedDict, total=False): + commitments: list[str] + response: RecordsResponseFilter + start: int + end: int + programs: list[str] + records: list[str] + functions: list[str] + results_per_page: int + page: int + spent: bool + program: str + record: str + + +class OwnedRecordsResponseFilter(TypedDict, total=False): + commitment: bool + owner: bool + tag: bool + sender: bool + spent: bool + record_ciphertext: bool + block_height: bool + block_timestamp: bool + output_index: bool + record_name: bool + function_name: bool + program_name: bool + transition_id: bool + transaction_id: bool + transaction_index: bool + transition_index: bool + + +class OwnedFilter(TypedDict, total=False): + uuid: str + unspent: bool + decrypt: bool + filter: RecordsFilter + responseFilter: OwnedRecordsResponseFilter + nonces: list[str] + + +class OwnedRecord(TypedDict, total=False): + block_height: int + block_timestamp: int + commitment: str + function_name: str + output_index: int + owner: str + program_name: str + record_ciphertext: str + record_plaintext: str + record_name: str + sender: str + spent: bool + tag: str + transaction_id: str + transition_id: str + transaction_index: int + transition_index: int + + +# --------------------------------------------------------------------------- +# Pure helpers +# --------------------------------------------------------------------------- + +def compute_uuid(view_key: Any) -> Any: + """Compute the RecordScanner UUID from a ViewKey using Poseidon4. + + Returns a Field with domain separator "RecordScannerV0". + This is the golden KAT: + APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH + -> 7884164224800444110633570141944665301008802280502652120359195870264061098703field + """ + from .mainnet import Field, Poseidon4 # type: ignore[attr-defined] + domain_sep = Field.domain_separator("RecordScannerV0") + vk_field = view_key.to_field() + one = Field.one() + hasher = Poseidon4() + return hasher.hash([domain_sep, vk_field, one]) + + +def uuid_is_valid(uuid: str) -> bool: + """Return True if uuid is a valid Field string (e.g. '1234...field').""" + try: + from .mainnet import Field # type: ignore[attr-defined] + Field.from_string(uuid) + return True + except Exception: + return False + + +def normalize_api_key( + api_key: str | tuple[str, str] | dict[str, str] | None, +) -> dict[str, str] | None: + """Normalize api_key to {'header': str, 'value': str} or None.""" + if api_key is None: + return None + if isinstance(api_key, str): + return {"header": "X-Provable-API-Key", "value": api_key} + if isinstance(api_key, tuple): + header, value = api_key + return {"header": header, "value": value} + # dict + return dict(api_key) diff --git a/sdk/python/aleo/async_record_scanner.py b/sdk/python/aleo/async_record_scanner.py new file mode 100644 index 0000000..3982881 --- /dev/null +++ b/sdk/python/aleo/async_record_scanner.py @@ -0,0 +1,493 @@ +"""Asynchronous RecordScanner (httpx-based) for delegated record scanning.""" +from __future__ import annotations + +import json +from typing import Any +from urllib.parse import urlparse + +from ._client_common import jwt_expired +from ._scanner_common import ( + DecryptionNotEnabledError, + OwnedFilter, + OwnedRecord, + RecordNotFoundError, + RecordScannerRequestError, + RecordsFilter, + UUIDError, + ViewKeyNotStoredError, + compute_uuid, + normalize_api_key, + uuid_is_valid, +) +from .security import encrypt_registration_request + + +class AsyncRecordScanner: + """Asynchronous record scanner client (httpx-based). + + Parameters + ---------- + url: + Base URL for the record scanner service (without network suffix). + Must NOT end with ``/mainnet`` or ``/testnet``. + network: + Network name (default ``"mainnet"``). + api_key: + API key as a string, ``(header, value)`` tuple, or + ``{"header": ..., "value": ...}`` dict. + consumer_id: + Consumer ID paired with *api_key* for JWT refresh. + jwt_data: + Pre-populated JWT dict ``{"jwt": str, "expiration": int (ms)}``. + view_keys: + Initial list of ViewKey objects to store. + account: + Account whose view key is used for UUID and scanning. + cache_view_keys_on_register: + If True, store the view key when registering (default True). + auto_re_register: + If True, re-register on 422 responses and retry once. + decrypt_enabled: + If True, decrypt records in-place for ``owned()`` calls. + transport: + Optional ``httpx.AsyncBaseTransport`` instance passed to + ``httpx.AsyncClient(transport=...)``. + """ + + def __init__( + self, + url: str, + *, + network: str = "mainnet", + api_key: str | tuple[str, str] | dict[str, str] | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + view_keys: list[Any] | None = None, + account: Any | None = None, + cache_view_keys_on_register: bool = True, + auto_re_register: bool = False, + decrypt_enabled: bool = False, + transport: Any = None, + ) -> None: + try: + import httpx # type: ignore[import] + except ImportError: + raise ImportError( + "httpx is required for AsyncRecordScanner. " + "Install with: pip install aleo[async]" + ) from None + + # Validate: URL must not end with a network suffix + stripped = url.rstrip("/") + if stripped.endswith("/mainnet") or stripped.endswith("/testnet"): + raise ValueError( + "The record scanning url should not include the specific network. " + f"Remove '/{stripped.rsplit('/', 1)[-1]}' from the url." + ) + + self.url: str = f"{url}/{network}" + self._origin: str = urlparse(url).scheme + "://" + urlparse(url).netloc + self._network: str = network + + self._api_key: dict[str, str] | None = normalize_api_key(api_key) + self.consumer_id: str | None = consumer_id + self.jwt_data: dict[str, Any] | None = jwt_data + self.cache_view_keys_on_register: bool = cache_view_keys_on_register + self.auto_re_register: bool = auto_re_register + self.decrypt_enabled: bool = decrypt_enabled + + # UUID and view key storage + self._uuid: Any | None = None # Field | None + self._view_keys: dict[str, Any] = {} # uuid_str -> ViewKey + self._account: Any | None = None + + # Build httpx client + if transport is not None: + self._client: Any = httpx.AsyncClient(transport=transport) + else: + self._client = httpx.AsyncClient() + + # Load initial view keys + if view_keys: + for vk in view_keys: + self.add_view_key(vk) + + # Load account (mirrors TS: set, setUuid, addViewKey) + if account is not None: + self._account = account + self.set_uuid(account.view_key) + self.add_view_key(account.view_key) + + # ── Mutators ───────────────────────────────────────────────────────── + + def set_api_key(self, api_key: str | tuple[str, str] | dict[str, str]) -> None: + self._api_key = normalize_api_key(api_key) + + def set_consumer_id(self, consumer_id: str) -> None: + self.consumer_id = consumer_id + + def set_jwt_data(self, jwt_data: dict[str, Any] | None) -> None: + self.jwt_data = jwt_data + + def set_auto_re_register(self, enabled: bool) -> None: + self.auto_re_register = enabled + + def set_decrypt_enabled(self, enabled: bool) -> None: + self.decrypt_enabled = enabled + + def add_view_key(self, view_key: Any) -> None: + """Store view_key keyed by its computed UUID string.""" + uuid_field = compute_uuid(view_key) + self._view_keys[str(uuid_field)] = view_key + + def remove_view_key(self, uuid: str) -> None: + self._view_keys.pop(uuid, None) + + def set_uuid(self, key_material: Any) -> None: + """Set UUID from a Field or ViewKey.""" + from .mainnet import Field # type: ignore[attr-defined] + if isinstance(key_material, Field): + self._uuid = key_material + else: + self._uuid = compute_uuid(key_material) + + def set_account(self, account: Any) -> None: + """Set account: mirrors TS (set, setUuid, addViewKey, remove old uuid).""" + old_uuid: str | None = None + if self._account is not None: + try: + old_uuid = str(compute_uuid(self._account.view_key)) + except Exception: + pass + + self._account = account + self.set_uuid(account.view_key) + self.add_view_key(account.view_key) + + if old_uuid and old_uuid != str(self._uuid): + self.remove_view_key(old_uuid) + + # ── Internal HTTP ───────────────────────────────────────────────────── + + async def _build_headers(self) -> dict[str, str]: + """Build authentication headers, refreshing JWT if needed.""" + hdrs: dict[str, str] = {"Content-Type": "application/json"} + + if self._api_key: + hdrs[self._api_key["header"]] = self._api_key["value"] + + needs_refresh = not self.jwt_data or jwt_expired(self.jwt_data) + if needs_refresh: + if self._api_key and self.consumer_id: + jwt_url = f"{self._origin}/jwts/{self.consumer_id}" + jwt_hdrs = {self._api_key["header"]: self._api_key["value"]} + resp = await self._client.post(jwt_url, headers=jwt_hdrs) + if resp.is_success: + auth = resp.headers.get("Authorization") or resp.headers.get("authorization") + body = resp.json() + if auth: + self.jwt_data = {"jwt": auth, "expiration": body["exp"] * 1000} + + if self.jwt_data and self.jwt_data.get("jwt"): + hdrs["Authorization"] = self.jwt_data["jwt"] + + return hdrs + + async def _get_json(self, url: str) -> Any: + hdrs = await self._build_headers() + resp = await self._client.get(url, headers=hdrs) + if not resp.is_success: + raise RecordScannerRequestError(resp.text, resp.status_code) + return resp.json() + + async def _post_json(self, url: str, body: str) -> Any: + hdrs = await self._build_headers() + resp = await self._client.post(url, content=body.encode(), headers=hdrs) + if not resp.is_success: + raise RecordScannerRequestError(resp.text, resp.status_code) + return resp + + # ── UUID helpers ───────────────────────────────────────────────────── + + def _resolve_and_validate_uuid(self, uuid_param: str | None = None) -> str: + candidate: str | None = uuid_param + if candidate is None and self._uuid is not None: + candidate = str(self._uuid) + if candidate is None: + raise UUIDError("No UUID configured. Call register() or set_uuid() first.") + if not uuid_is_valid(candidate): + raise UUIDError( + f"UUID '{candidate}' is invalid (not a valid field string).", + uuid=candidate, + ) + return candidate + + # ── Registration ───────────────────────────────────────────────────── + + async def register_encrypted( + self, + view_key: Any, + start_block: int = 0, + ) -> dict[str, Any]: + """Register a view key for scanning using sealed-box encryption.""" + try: + pubkey_data = await self._get_json(f"{self.url}/pubkey") + key_id: str = pubkey_data["key_id"] + public_key: str = pubkey_data["public_key"] + + ciphertext = encrypt_registration_request(public_key, view_key, start_block) + payload = json.dumps({"key_id": key_id, "ciphertext": ciphertext}) + + resp = await self._post_json(f"{self.url}/register/encrypted", payload) + data: dict[str, Any] = resp.json() + + if self._uuid is None: + from .mainnet import Field # type: ignore[attr-defined] + try: + self._uuid = Field.from_string(data["uuid"]) + except Exception: + pass + + if self.cache_view_keys_on_register: + self.add_view_key(view_key) + + return {"ok": True, "data": data} + + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + async def register(self, view_key: Any, start_block: int = 0) -> dict[str, Any]: + """Alias for register_encrypted.""" + return await self.register_encrypted(view_key, start_block) + + # ── Revoke ─────────────────────────────────────────────────────────── + + async def revoke(self, uuid: str | None = None) -> dict[str, Any]: + """Revoke a registration.""" + resolved = self._resolve_and_validate_uuid(uuid) + body = json.dumps(resolved) + await self._post_json(f"{self.url}/revoke", body) + + if self._uuid is not None and str(self._uuid) == resolved: + self._uuid = None + self._view_keys.pop(resolved, None) + if self._account is not None: + try: + if str(compute_uuid(self._account.view_key)) == resolved: + self._account = None + except Exception: + pass + + return {"ok": True, "data": {"status": "OK"}} + + # ── Status ─────────────────────────────────────────────────────────── + + async def status(self, uuid: str | None = None) -> dict[str, Any]: + """Get scanning status for a UUID.""" + resolved = self._resolve_and_validate_uuid(uuid) + body = json.dumps(resolved) + resp = await self._post_json(f"{self.url}/status", body) + return {"ok": True, "data": resp.json()} + + # ── Owned records ───────────────────────────────────────────────────── + + async def owned(self, filter: OwnedFilter) -> dict[str, Any]: + """Fetch owned records matching the given filter.""" + filter_uuid = filter.get("uuid", "") + if filter_uuid and uuid_is_valid(filter_uuid): + resolved_uuid = filter_uuid + elif self._uuid is not None: + resolved_uuid = str(self._uuid) + else: + raise UUIDError( + "No UUID in filter or configured. Call register() or set_uuid() first.", + filter=dict(filter), + ) + + filter["uuid"] = resolved_uuid + body = json.dumps(filter) + + hdrs = await self._build_headers() + resp = await self._client.post( + f"{self.url}/records/owned", content=body.encode(), headers=hdrs + ) + + if resp.status_code == 422 and self.auto_re_register: + vk = self._view_keys.get(resolved_uuid) + if vk is not None: + await self.register_encrypted(vk, 0) + hdrs = await self._build_headers() + resp = await self._client.post( + f"{self.url}/records/owned", content=body.encode(), headers=hdrs + ) + + if not resp.is_success: + return { + "ok": False, + "status": resp.status_code, + "error": {"message": resp.text, "status": resp.status_code}, + } + + records: list[OwnedRecord] = resp.json() + + if self.decrypt_enabled: + vk = self._view_keys.get(resolved_uuid) + if vk is not None: + self.decrypt(vk, records) + + return {"ok": True, "data": records} + + # ── Encrypted records ───────────────────────────────────────────────── + + async def encrypted(self, records_filter: RecordsFilter) -> dict[str, Any]: + """Fetch encrypted records matching the given filter.""" + body = json.dumps(records_filter) + try: + resp = await self._post_json(f"{self.url}/records/encrypted", body) + return {"ok": True, "data": resp.json()} + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + async def encrypted_records(self, records_filter: RecordsFilter) -> list[Any]: + """Throwing wrapper around encrypted().""" + result = await self.encrypted(records_filter) + if result["ok"]: + return result["data"] # type: ignore[return-value] + err = result.get("error", {}) + raise RecordScannerRequestError( + err.get("message", "encrypted_records failed"), + result.get("status", 500), + ) + + # ── Serial numbers & tags ───────────────────────────────────────────── + + async def check_serial_numbers( + self, serial_numbers: list[str] + ) -> dict[str, Any]: + """Check whether serial numbers are spent.""" + body = json.dumps(serial_numbers) + try: + resp = await self._post_json(f"{self.url}/records/sns", body) + return {"ok": True, "data": resp.json()} + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + async def serial_numbers(self, serial_numbers: list[str]) -> dict[str, Any]: + """Alias for check_serial_numbers.""" + return await self.check_serial_numbers(serial_numbers) + + async def check_tags(self, tags: list[str]) -> dict[str, Any]: + """Check whether tags are spent.""" + body = json.dumps(tags) + try: + resp = await self._post_json(f"{self.url}/records/tags", body) + return {"ok": True, "data": resp.json()} + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + async def tags(self, tags: list[str]) -> dict[str, Any]: + """Alias for check_tags.""" + return await self.check_tags(tags) + + # ── Convenience ─────────────────────────────────────────────────────── + + def decrypt(self, view_key: Any, records: list[Any]) -> None: + """Decrypt record_ciphertext fields in-place.""" + from .mainnet import RecordCiphertext # type: ignore[attr-defined] + for record in records: + ct = record.get("record_ciphertext", "").strip() + if not ct: + continue + try: + plaintext = RecordCiphertext.from_string(ct).decrypt(view_key) + record["record_plaintext"] = str(plaintext) + except Exception: + pass + + async def find_records(self, filter: OwnedFilter) -> list[OwnedRecord]: + """Call owned() and return the data list, or raise.""" + result = await self.owned(filter) + if result["ok"]: + return result["data"] # type: ignore[return-value] + err = result.get("error", {}) + raise RecordScannerRequestError( + err.get("message", "find_records failed"), + result.get("status", 500), + ) + + async def find_record(self, filter: OwnedFilter) -> OwnedRecord: + """Return the first record from find_records(), or raise RecordNotFoundError.""" + records = await self.find_records(filter) + if not records: + raise RecordNotFoundError("No matching record found.") + return records[0] + + async def find_credits_record( + self, microcredits: int, filter: OwnedFilter + ) -> OwnedRecord: + """Find first credits.aleo record with >= microcredits.""" + filter_uuid = filter.get("uuid", "") + if filter_uuid and uuid_is_valid(filter_uuid): + resolved_uuid = filter_uuid + elif self._uuid is not None: + resolved_uuid = str(self._uuid) + else: + raise UUIDError("No UUID configured.") + + if not self.decrypt_enabled: + raise DecryptionNotEnabledError( + "decrypt_enabled must be True to use find_credits_record." + ) + + vk = self._view_keys.get(resolved_uuid) + if vk is None: + raise ViewKeyNotStoredError( + f"No view key stored for uuid '{resolved_uuid}'.", + uuid=resolved_uuid, + ) + + credits_filter: OwnedFilter = { + "unspent": filter.get("unspent", True), # type: ignore[typeddict-item] + "filter": { # type: ignore[typeddict-item] + "program": "credits.aleo", + "record": "credits", + **filter.get("filter", {}), # type: ignore[arg-type] + }, + "uuid": resolved_uuid, + } + if "responseFilter" in filter: + credits_filter["responseFilter"] = filter["responseFilter"] # type: ignore[typeddict-item] + + records = await self.find_records(credits_filter) + + from .mainnet import RecordPlaintext # type: ignore[attr-defined] + for record in records: + pt_str = record.get("record_plaintext", "") + if not pt_str: + continue + try: + pt = RecordPlaintext.from_string(pt_str) + if pt.microcredits >= microcredits: + return record + except Exception: + pass + + raise RecordNotFoundError( + f"No credits record found with >= {microcredits} microcredits." + ) diff --git a/sdk/python/aleo/record_scanner.py b/sdk/python/aleo/record_scanner.py new file mode 100644 index 0000000..da392eb --- /dev/null +++ b/sdk/python/aleo/record_scanner.py @@ -0,0 +1,609 @@ +"""Synchronous RecordScanner (requests-based) for delegated record scanning.""" +from __future__ import annotations + +import json +from typing import Any +from urllib.parse import urlparse + +import requests + +from ._client_common import jwt_expired +from ._scanner_common import ( + DecryptionNotEnabledError, + OwnedFilter, + OwnedRecord, + RecordNotFoundError, + RecordScannerRequestError, + RecordsFilter, + UUIDError, + ViewKeyNotStoredError, + compute_uuid, + normalize_api_key, + uuid_is_valid, +) +from .security import encrypt_registration_request + + +class RecordScanner: + """Synchronous record scanner client (requests-based). + + Parameters + ---------- + url: + Base URL for the record scanner service (without network suffix). + Must NOT end with ``/mainnet`` or ``/testnet``. + network: + Network name (default ``"mainnet"``). + api_key: + API key as a string, ``(header, value)`` tuple, or + ``{"header": ..., "value": ...}`` dict. + consumer_id: + Consumer ID paired with *api_key* for JWT refresh. + jwt_data: + Pre-populated JWT dict ``{"jwt": str, "expiration": int (ms)}``. + view_keys: + Initial list of ViewKey objects to store. + account: + Account whose view key is used for UUID and scanning. + cache_view_keys_on_register: + If True, store the view key when registering (default True). + auto_re_register: + If True, re-register on 422 responses and retry once. + decrypt_enabled: + If True, decrypt records in-place for ``owned()`` calls. + transport: + Optional callable ``(method, url, **kwargs) -> requests.Response`` + for testing or custom HTTP routing. + """ + + def __init__( + self, + url: str, + *, + network: str = "mainnet", + api_key: str | tuple[str, str] | dict[str, str] | None = None, + consumer_id: str | None = None, + jwt_data: dict[str, Any] | None = None, + view_keys: list[Any] | None = None, + account: Any | None = None, + cache_view_keys_on_register: bool = True, + auto_re_register: bool = False, + decrypt_enabled: bool = False, + transport: Any = None, + ) -> None: + # Validate: URL must not end with a network suffix + stripped = url.rstrip("/") + if stripped.endswith("/mainnet") or stripped.endswith("/testnet"): + raise ValueError( + "The record scanning url should not include the specific network. " + f"Remove '/{stripped.rsplit('/', 1)[-1]}' from the url." + ) + + self.url: str = f"{url}/{network}" + self._origin: str = urlparse(url).scheme + "://" + urlparse(url).netloc + self._network: str = network + + self._api_key: dict[str, str] | None = normalize_api_key(api_key) + self.consumer_id: str | None = consumer_id + self.jwt_data: dict[str, Any] | None = jwt_data + self.cache_view_keys_on_register: bool = cache_view_keys_on_register + self.auto_re_register: bool = auto_re_register + self.decrypt_enabled: bool = decrypt_enabled + + self._transport: Any = transport + self._session: requests.Session = requests.Session() + + # UUID and view key storage + self._uuid: Any | None = None # Field | None + self._view_keys: dict[str, Any] = {} # uuid_str -> ViewKey + self._account: Any | None = None + + # Load initial view keys + if view_keys: + for vk in view_keys: + self.add_view_key(vk) + + # Load account (mirrors TS: set, setUuid, addViewKey) + if account is not None: + self._account = account + self.set_uuid(account.view_key) + self.add_view_key(account.view_key) + + # ── Internal HTTP ───────────────────────────────────────────────────── + + def _http(self, method: str, url: str, **kwargs: Any) -> requests.Response: + if callable(self._transport): + return self._transport(method, url, **kwargs) # type: ignore[return-value] + return self._session.request(method, url, **kwargs) # type: ignore[return-value] + + def _build_headers(self) -> dict[str, str]: + """Build authentication headers, refreshing JWT if needed.""" + hdrs: dict[str, str] = {"Content-Type": "application/json"} + + # Always attach api_key header if set + if self._api_key: + hdrs[self._api_key["header"]] = self._api_key["value"] + + # JWT logic + needs_refresh = not self.jwt_data or jwt_expired(self.jwt_data) + if needs_refresh: + if self._api_key and self.consumer_id: + # Refresh JWT + jwt_url = f"{self._origin}/jwts/{self.consumer_id}" + jwt_hdrs = {self._api_key["header"]: self._api_key["value"]} + resp = self._http("POST", jwt_url, headers=jwt_hdrs) + if resp.ok: + auth = resp.headers.get("Authorization") or resp.headers.get("authorization") + body = resp.json() + if auth: + self.jwt_data = {"jwt": auth, "expiration": body["exp"] * 1000} + # else: no jwt and can't refresh — skip jwt header + + if self.jwt_data and self.jwt_data.get("jwt"): + hdrs["Authorization"] = self.jwt_data["jwt"] + + return hdrs + + def _get_json(self, url: str) -> Any: + hdrs = self._build_headers() + resp = self._http("GET", url, headers=hdrs) + if not resp.ok: + raise RecordScannerRequestError(resp.text, resp.status_code) + return resp.json() + + def _post_json(self, url: str, body: str) -> requests.Response: + hdrs = self._build_headers() + resp = self._http("POST", url, headers=hdrs, data=body) + if not resp.ok: + raise RecordScannerRequestError(resp.text, resp.status_code) + return resp + + def _post_raw(self, url: str, body: str) -> requests.Response: + """POST without raising on non-2xx (caller inspects status).""" + hdrs = self._build_headers() + return self._http("POST", url, headers=hdrs, data=body) + + # ── Mutators ───────────────────────────────────────────────────────── + + def set_api_key(self, api_key: str | tuple[str, str] | dict[str, str]) -> None: + self._api_key = normalize_api_key(api_key) + + def set_consumer_id(self, consumer_id: str) -> None: + self.consumer_id = consumer_id + + def set_jwt_data(self, jwt_data: dict[str, Any] | None) -> None: + self.jwt_data = jwt_data + + def set_auto_re_register(self, enabled: bool) -> None: + self.auto_re_register = enabled + + def set_decrypt_enabled(self, enabled: bool) -> None: + self.decrypt_enabled = enabled + + def add_view_key(self, view_key: Any) -> None: + """Store view_key keyed by its computed UUID string.""" + uuid_field = compute_uuid(view_key) + self._view_keys[str(uuid_field)] = view_key + + def remove_view_key(self, uuid: str) -> None: + self._view_keys.pop(uuid, None) + + def set_uuid(self, key_material: Any) -> None: + """Set UUID from a Field or ViewKey. + + If key_material is a ViewKey, compute the UUID via Poseidon4. + If it's a Field, use it directly. + """ + from .mainnet import Field # type: ignore[attr-defined] + if isinstance(key_material, Field): + self._uuid = key_material + else: + # Assume ViewKey + self._uuid = compute_uuid(key_material) + + def set_account(self, account: Any) -> None: + """Set account: mirrors TS (set, setUuid, addViewKey, remove old uuid).""" + old_uuid: str | None = None + if self._account is not None: + try: + old_uuid = str(compute_uuid(self._account.view_key)) + except Exception: + pass + + self._account = account + self.set_uuid(account.view_key) + self.add_view_key(account.view_key) + + if old_uuid and old_uuid != str(self._uuid): + self.remove_view_key(old_uuid) + + # ── UUID helpers ───────────────────────────────────────────────────── + + def _resolve_uuid(self, uuid_param: str | None = None) -> str: + """Resolve a UUID string, falling back to self._uuid.""" + if uuid_param and uuid_is_valid(uuid_param): + return uuid_param + if self._uuid is not None: + return str(self._uuid) + raise UUIDError("No UUID configured. Call register() or set_uuid() first.") + + def _resolve_and_validate_uuid(self, uuid_param: str | None = None) -> str: + """Resolve UUID and validate; raise UUIDError on failure.""" + # Prefer param if explicitly provided + candidate: str | None = uuid_param + if candidate is None and self._uuid is not None: + candidate = str(self._uuid) + if candidate is None: + raise UUIDError("No UUID configured. Call register() or set_uuid() first.") + if not uuid_is_valid(candidate): + raise UUIDError( + f"UUID '{candidate}' is invalid (not a valid field string).", + uuid=candidate, + ) + return candidate + + # ── Registration ───────────────────────────────────────────────────── + + def register_encrypted( + self, + view_key: Any, + start_block: int = 0, + ) -> dict[str, Any]: + """Register a view key for scanning using sealed-box encryption. + + Returns ``{"ok": True, "data": {"uuid": str, "status": str|None}}`` + or ``{"ok": False, "status": int, "error": {"message": ..., "status": int}}``. + """ + try: + # Step 1: GET pubkey + pubkey_data = self._get_json(f"{self.url}/pubkey") + key_id: str = pubkey_data["key_id"] + public_key: str = pubkey_data["public_key"] + + # Step 2: Encrypt + ciphertext = encrypt_registration_request(public_key, view_key, start_block) + + # Step 3: POST to /register/encrypted + payload = json.dumps({"key_id": key_id, "ciphertext": ciphertext}) + resp = self._post_json(f"{self.url}/register/encrypted", payload) + data: dict[str, Any] = resp.json() + + # Step 4: Update state + if self._uuid is None: + from .mainnet import Field # type: ignore[attr-defined] + try: + self._uuid = Field.from_string(data["uuid"]) + except Exception: + pass + + if self.cache_view_keys_on_register: + self.add_view_key(view_key) + + return {"ok": True, "data": data} + + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + def register(self, view_key: Any, start_block: int = 0) -> dict[str, Any]: + """Alias for register_encrypted.""" + return self.register_encrypted(view_key, start_block) + + # ── Revoke ─────────────────────────────────────────────────────────── + + def revoke(self, uuid: str | None = None) -> dict[str, Any]: + """Revoke a registration. + + Returns ``{"ok": True, "data": {"status": "OK"}}``. + """ + resolved = self._resolve_and_validate_uuid(uuid) + + body = json.dumps(resolved) # quoted JSON string: '"uuid_str"' + self._post_json(f"{self.url}/revoke", body) + + # Clear local state if our uuid was revoked + if self._uuid is not None and str(self._uuid) == resolved: + self._uuid = None + self._view_keys.pop(resolved, None) + if self._account is not None: + try: + if str(compute_uuid(self._account.view_key)) == resolved: + self._account = None + except Exception: + pass + + return {"ok": True, "data": {"status": "OK"}} + + # ── Status ─────────────────────────────────────────────────────────── + + def status(self, uuid: str | None = None) -> dict[str, Any]: + """Get scanning status for a UUID. + + Returns ``{"ok": True, "data": {"synced": bool, "percentage": float}}``. + """ + resolved = self._resolve_and_validate_uuid(uuid) + body = json.dumps(resolved) + resp = self._post_json(f"{self.url}/status", body) + return {"ok": True, "data": resp.json()} + + # ── Owned records ───────────────────────────────────────────────────── + + def owned(self, filter: OwnedFilter) -> dict[str, Any]: + """Fetch owned records matching the given filter. + + Mutates ``filter["uuid"]`` to the resolved UUID. + Returns ``{"ok": True, "data": [OwnedRecord, ...]}``. + """ + # UUID resolution + filter_uuid = filter.get("uuid", "") + if filter_uuid and uuid_is_valid(filter_uuid): + resolved_uuid = filter_uuid + elif self._uuid is not None: + resolved_uuid = str(self._uuid) + else: + raise UUIDError( + "No UUID in filter or configured. Call register() or set_uuid() first.", + filter=dict(filter), + ) + + # Mutate filter + filter["uuid"] = resolved_uuid + + body = json.dumps(filter) + + # Try once (with possible 422 retry) + hdrs = self._build_headers() + resp = self._http("POST", f"{self.url}/records/owned", headers=hdrs, data=body) + + if resp.status_code == 422 and self.auto_re_register: + # Re-register if we have a view key for this uuid + vk = self._view_keys.get(resolved_uuid) + if vk is not None: + self.register_encrypted(vk, 0) + hdrs = self._build_headers() + resp = self._http( + "POST", f"{self.url}/records/owned", headers=hdrs, data=body + ) + + if not resp.ok: + return { + "ok": False, + "status": resp.status_code, + "error": {"message": resp.text, "status": resp.status_code}, + } + + records: list[OwnedRecord] = resp.json() + + # Decrypt in-place if enabled + if self.decrypt_enabled: + vk = self._view_keys.get(resolved_uuid) + if vk is not None: + self.decrypt(vk, records) + + return {"ok": True, "data": records} + + # ── Encrypted records ───────────────────────────────────────────────── + + def encrypted(self, records_filter: RecordsFilter) -> dict[str, Any]: + """Fetch encrypted records matching the given filter. + + Returns ``{"ok": True, "data": [...]}``. + """ + body = json.dumps(records_filter) + try: + resp = self._post_json(f"{self.url}/records/encrypted", body) + return {"ok": True, "data": resp.json()} + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + def encrypted_records(self, records_filter: RecordsFilter) -> list[Any]: + """Throwing wrapper around encrypted(). Returns list or raises.""" + result = self.encrypted(records_filter) + if result["ok"]: + return result["data"] # type: ignore[return-value] + err = result.get("error", {}) + raise RecordScannerRequestError( + err.get("message", "encrypted_records failed"), + result.get("status", 500), + ) + + # ── Serial numbers & tags ───────────────────────────────────────────── + + def check_serial_numbers( + self, serial_numbers: list[str] + ) -> dict[str, Any]: + """Check whether serial numbers are spent. + + Returns ``{"ok": True, "data": dict[str, bool]}``. + """ + body = json.dumps(serial_numbers) + try: + resp = self._post_json(f"{self.url}/records/sns", body) + return {"ok": True, "data": resp.json()} + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + def serial_numbers(self, serial_numbers: list[str]) -> dict[str, Any]: + """Alias for check_serial_numbers.""" + return self.check_serial_numbers(serial_numbers) + + def check_tags(self, tags: list[str]) -> dict[str, Any]: + """Check whether tags are spent. + + Returns ``{"ok": True, "data": dict[str, bool]}``. + """ + body = json.dumps(tags) + try: + resp = self._post_json(f"{self.url}/records/tags", body) + return {"ok": True, "data": resp.json()} + except RecordScannerRequestError as exc: + return { + "ok": False, + "status": exc.status, + "error": {"message": str(exc), "status": exc.status}, + } + + def tags(self, tags: list[str]) -> dict[str, Any]: + """Alias for check_tags.""" + return self.check_tags(tags) + + # ── Convenience: decrypt, find_records, find_credits_record ────────── + + def decrypt(self, view_key: Any, records: list[Any]) -> None: + """Decrypt record_ciphertext fields in-place.""" + from .mainnet import RecordCiphertext # type: ignore[attr-defined] + for record in records: + ct = record.get("record_ciphertext", "").strip() + if not ct: + continue + try: + plaintext = RecordCiphertext.from_string(ct).decrypt(view_key) + record["record_plaintext"] = str(plaintext) + except Exception: + pass # wrong key or invalid ciphertext — skip silently + + def find_records(self, filter: OwnedFilter) -> list[OwnedRecord]: + """Call owned() and return the data list, or raise RecordScannerRequestError.""" + result = self.owned(filter) + if result["ok"]: + return result["data"] # type: ignore[return-value] + err = result.get("error", {}) + raise RecordScannerRequestError( + err.get("message", "find_records failed"), + result.get("status", 500), + ) + + def find_record(self, filter: OwnedFilter) -> OwnedRecord: + """Return the first record from find_records(), or raise RecordNotFoundError.""" + records = self.find_records(filter) + if not records: + raise RecordNotFoundError("No matching record found.") + return records[0] + + def find_credits_record( + self, microcredits: int, filter: OwnedFilter + ) -> OwnedRecord: + """Find first credits.aleo record with >= microcredits. + + Requires decrypt_enabled=True and a stored view key for the UUID. + """ + # Resolve UUID + filter_uuid = filter.get("uuid", "") + if filter_uuid and uuid_is_valid(filter_uuid): + resolved_uuid = filter_uuid + elif self._uuid is not None: + resolved_uuid = str(self._uuid) + else: + raise UUIDError("No UUID configured.") + + if not self.decrypt_enabled: + raise DecryptionNotEnabledError( + "decrypt_enabled must be True to use find_credits_record." + ) + + vk = self._view_keys.get(resolved_uuid) + if vk is None: + raise ViewKeyNotStoredError( + f"No view key stored for uuid '{resolved_uuid}'.", + uuid=resolved_uuid, + ) + + # Build credits filter + credits_filter: OwnedFilter = { + "unspent": filter.get("unspent", True), # type: ignore[typeddict-item] + "filter": { # type: ignore[typeddict-item] + "program": "credits.aleo", + "record": "credits", + **filter.get("filter", {}), # type: ignore[arg-type] + }, + "uuid": resolved_uuid, + } + if "responseFilter" in filter: + credits_filter["responseFilter"] = filter["responseFilter"] # type: ignore[typeddict-item] + + records = self.find_records(credits_filter) + + from .mainnet import RecordPlaintext # type: ignore[attr-defined] + for record in records: + pt_str = record.get("record_plaintext", "") + if not pt_str: + continue + try: + pt = RecordPlaintext.from_string(pt_str) + if pt.microcredits >= microcredits: + return record + except Exception: + pass + + raise RecordNotFoundError( + f"No credits record found with >= {microcredits} microcredits." + ) + + def find_credits_records( + self, microcredit_amounts: list[int], filter: OwnedFilter + ) -> list[OwnedRecord]: + """Find credits.aleo records whose microcredits are in microcredit_amounts. + + Requires decrypt_enabled=True and a stored view key for the UUID. + """ + # Resolve UUID + filter_uuid = filter.get("uuid", "") + if filter_uuid and uuid_is_valid(filter_uuid): + resolved_uuid = filter_uuid + elif self._uuid is not None: + resolved_uuid = str(self._uuid) + else: + raise UUIDError("No UUID configured.") + + if not self.decrypt_enabled: + raise DecryptionNotEnabledError( + "decrypt_enabled must be True to use find_credits_records." + ) + + vk = self._view_keys.get(resolved_uuid) + if vk is None: + raise ViewKeyNotStoredError( + f"No view key stored for uuid '{resolved_uuid}'.", + uuid=resolved_uuid, + ) + + credits_filter: OwnedFilter = { + "unspent": filter.get("unspent", True), # type: ignore[typeddict-item] + "filter": { # type: ignore[typeddict-item] + "program": "credits.aleo", + "record": "credits", + **filter.get("filter", {}), # type: ignore[arg-type] + }, + "uuid": resolved_uuid, + } + if "responseFilter" in filter: + credits_filter["responseFilter"] = filter["responseFilter"] # type: ignore[typeddict-item] + + records = self.find_records(credits_filter) + + from .mainnet import RecordPlaintext # type: ignore[attr-defined] + amounts_set = set(microcredit_amounts) + result: list[OwnedRecord] = [] + for record in records: + pt_str = record.get("record_plaintext", "") + if not pt_str: + continue + try: + pt = RecordPlaintext.from_string(pt_str) + if pt.microcredits in amounts_set: + result.append(record) + except Exception: + pass + + return result diff --git a/sdk/python/aleo/security.py b/sdk/python/aleo/security.py index fa56afa..6d1d1ea 100644 --- a/sdk/python/aleo/security.py +++ b/sdk/python/aleo/security.py @@ -2,6 +2,8 @@ from __future__ import annotations import base64 +import struct +from typing import Any def encrypt_proving_request(public_key_b64: str, pr_bytes: bytes) -> str: @@ -22,3 +24,27 @@ def encrypt_proving_request(public_key_b64: str, pr_bytes: bytes) -> str: box = SealedBox(recipient_key) ciphertext = box.encrypt(pr_bytes) return base64.b64encode(ciphertext).decode("ascii") + + +def encrypt_registration_request(public_key_b64: str, view_key: Any, start_block: int) -> str: + """Encrypt view_key LE bytes (32) + start_block u32 LE (4) with NaCl sealed box. + + Returns standard (non-URL-safe) base64. + Wire shape: 32 (ephemeral pk) + 16 (MAC) + 36 (plaintext) = 84 bytes total ciphertext. + """ + try: + from nacl.public import PublicKey, SealedBox # type: ignore[import] + except ImportError: + raise ImportError( + "PyNaCl is required for record scanner registration. " + "Install with: pip install aleo[dps]" + ) from None + + vk_bytes = bytes(view_key.bytes()) # 32 bytes, LE + start_bytes = struct.pack("<I", start_block) # 4 bytes, LE u32 + plaintext = vk_bytes + start_bytes # 36 bytes total + + recipient_key = PublicKey(base64.b64decode(public_key_b64)) + box = SealedBox(recipient_key) + ciphertext = box.encrypt(plaintext) # 32 + 16 + 36 = 84 bytes + return base64.b64encode(ciphertext).decode("ascii") diff --git a/sdk/python/tests/test_record_scanner.py b/sdk/python/tests/test_record_scanner.py new file mode 100644 index 0000000..4fa7407 --- /dev/null +++ b/sdk/python/tests/test_record_scanner.py @@ -0,0 +1,629 @@ +"""Tests for RecordScanner (sync, requests-based). All mocked — no live network.""" +from __future__ import annotations + +import base64 +import json +from typing import Any + +import pytest +import responses as resp_lib + +from aleo.record_scanner import RecordScanner +from aleo._scanner_common import ( + compute_uuid, + uuid_is_valid, + DecryptionNotEnabledError, + RecordNotFoundError, + RecordScannerRequestError, + UUIDError, + ViewKeyNotStoredError, +) + +# --------------------------------------------------------------------------- +# KAT constants (from TS SDK tests/data/records.ts) +# --------------------------------------------------------------------------- + +GOLDEN_PRIVATE_KEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" +GOLDEN_UUID = "7884164224800444110633570141944665301008802280502652120359195870264061098703field" +VIEW_KEY_STRING = "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX" +RECORD_CIPHERTEXT_STRING = ( + "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzq" + "rtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5" + "pxm5cmxsv4un97q" +) +RECORD_VIEW_KEY_STRING = "4445718830394614891114647247073357094867447866913203502139893824059966201724field" + +CHECK_SNS_RESPONSE: dict[str, bool] = { + "1621694306596217216370326054181178914897851479837084979111511176605457690717field": True, + "5684626152578699086223993752521225507576791345254401210560771329591763880242field": False, +} + +CHECK_TAGS_RESPONSE: dict[str, bool] = { + "2965517500209150226508265073635793457193572667031485750956287906078711930968field": False, + "8421937347379608036510120951995833971195343843566214313082589116311107280540field": False, + "5941252181432651644402279701137165256963073258332916685063623109173576520831field": False, +} + +BASE_URL = "https://record-scanner.aleo.org" +HOST = f"{BASE_URL}/mainnet" + +# Real plaintext from decrypting RECORD_CIPHERTEXT_STRING with VIEW_KEY_STRING +RECORD_PLAINTEXT_STR = ( + "{\n owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,\n" + " microcredits: 1500000000000000u64.private,\n" + " _nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public,\n" + " _version: 0u8.public\n}" +) + +# Owned records fixture (two credits records with plaintext, different microcredits) +OWNED_CREDITS_RECORDS = [ + { + "record_ciphertext": RECORD_CIPHERTEXT_STRING, + "record_plaintext": RECORD_PLAINTEXT_STR, + "program_name": "credits.aleo", + "record_name": "credits", + "spent": False, + }, + { + "record_ciphertext": RECORD_CIPHERTEXT_STRING, + "record_plaintext": RECORD_PLAINTEXT_STR, + "program_name": "credits.aleo", + "record_name": "credits", + "spent": False, + }, +] + +OWNED_RECORDS = [ + {"commitment": "cm1abc", "record_ciphertext": RECORD_CIPHERTEXT_STRING, "spent": False}, + {"commitment": "cm1def", "record_ciphertext": RECORD_CIPHERTEXT_STRING, "spent": False}, +] + +# Pubkey fixture for registration tests +PUBKEY_FIXTURE: dict[str, str] = { + "key_id": "test-key-id-001", + "public_key": "", # filled in per-test with a generated key +} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _golden_view_key() -> Any: + from aleo.mainnet import PrivateKey + return PrivateKey.from_string(GOLDEN_PRIVATE_KEY).view_key + + +def _make_nacl_keypair() -> tuple[Any, str]: + """Return (nacl.PrivateKey, base64 public key).""" + from nacl.public import PrivateKey as NaclPrivateKey + sk = NaclPrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + return sk, pk_b64 + + +def _make_scanner(**kwargs: Any) -> RecordScanner: + return RecordScanner(BASE_URL, **kwargs) + + +# --------------------------------------------------------------------------- +# 1. URL construction +# --------------------------------------------------------------------------- + +def test_url_construction() -> None: + scanner = RecordScanner("https://record-scanner.aleo.org") + assert scanner.url == "https://record-scanner.aleo.org/mainnet" + + +def test_url_construction_custom_network() -> None: + scanner = RecordScanner("https://record-scanner.aleo.org", network="testnet") + assert scanner.url == "https://record-scanner.aleo.org/testnet" + + +# --------------------------------------------------------------------------- +# 2. Trailing network raises ValueError +# --------------------------------------------------------------------------- + +def test_trailing_network_raises_mainnet() -> None: + with pytest.raises(ValueError, match="should not include"): + RecordScanner("https://record-scanner.aleo.org/mainnet") + + +def test_trailing_network_raises_testnet() -> None: + with pytest.raises(ValueError, match="should not include"): + RecordScanner("https://record-scanner.aleo.org/testnet") + + +# --------------------------------------------------------------------------- +# 3. API key normalization — string +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_api_key_string() -> None: + """api_key as string → X-Provable-API-Key header on requests.""" + resp_lib.add(resp_lib.GET, f"{HOST}/pubkey", json={"key_id": "k", "public_key": "dGVzdA=="}) + resp_lib.add(resp_lib.POST, f"{HOST}/register/encrypted", json={"uuid": GOLDEN_UUID, "status": None}) + + scanner = _make_scanner(api_key="1234567890") + vk = _golden_view_key() + + # Use a real nacl key so encryption works + sk, pk_b64 = _make_nacl_keypair() + resp_lib.reset() + resp_lib.add(resp_lib.GET, f"{HOST}/pubkey", json={"key_id": "k", "public_key": pk_b64}) + resp_lib.add(resp_lib.POST, f"{HOST}/register/encrypted", json={"uuid": GOLDEN_UUID, "status": None}) + + result = scanner.register_encrypted(vk, 0) + assert result["ok"] is True + + # Check GET /pubkey had api key header + get_req = resp_lib.calls[0].request + assert get_req.headers.get("X-Provable-API-Key") == "1234567890" + + +# --------------------------------------------------------------------------- +# 4. API key normalization — custom tuple +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_api_key_custom_tuple() -> None: + """api_key as (header, value) tuple → correct header on requests.""" + sk, pk_b64 = _make_nacl_keypair() + resp_lib.add(resp_lib.GET, f"{HOST}/pubkey", json={"key_id": "k", "public_key": pk_b64}) + resp_lib.add(resp_lib.POST, f"{HOST}/register/encrypted", json={"uuid": GOLDEN_UUID, "status": None}) + + scanner = _make_scanner(api_key=("Some-API-Key", "myval")) + vk = _golden_view_key() + result = scanner.register_encrypted(vk, 0) + assert result["ok"] is True + + get_req = resp_lib.calls[0].request + assert get_req.headers.get("Some-API-Key") == "myval" + + +# --------------------------------------------------------------------------- +# 5. register_encrypted flow +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_register_encrypted_flow() -> None: + """Register flow: GET /pubkey, POST /register/encrypted, check ciphertext decrypts.""" + sk, pk_b64 = _make_nacl_keypair() + resp_lib.add(resp_lib.GET, f"{HOST}/pubkey", json={"key_id": "k1", "public_key": pk_b64}) + resp_lib.add( + resp_lib.POST, + f"{HOST}/register/encrypted", + json={"uuid": GOLDEN_UUID, "status": None}, + ) + + scanner = _make_scanner() + vk = _golden_view_key() + result = scanner.register_encrypted(vk, 0) + + assert result["ok"] is True + assert result["data"]["uuid"] == GOLDEN_UUID + + # Verify URL and method + assert "/pubkey" in resp_lib.calls[0].request.url + assert resp_lib.calls[0].request.method == "GET" + assert "/register/encrypted" in resp_lib.calls[1].request.url + assert resp_lib.calls[1].request.method == "POST" + assert resp_lib.calls[1].request.headers.get("Content-Type") == "application/json" + + # Verify ciphertext decrypts to vk_bytes + LE u32 + post_body = json.loads(resp_lib.calls[1].request.body) + ciphertext_b64 = post_body["ciphertext"] + raw = base64.b64decode(ciphertext_b64) + + # Wire shape: 32 (epk) + 16 (MAC) + 36 (plaintext) = 84 bytes + assert len(raw) == 84 + + from nacl.public import SealedBox + plaintext = SealedBox(sk).decrypt(raw) + assert len(plaintext) == 36 + + vk_bytes = bytes(vk.bytes()) + import struct + start_bytes = struct.pack("<I", 0) + assert plaintext == vk_bytes + start_bytes + + +@resp_lib.activate +def test_register_encrypted_distinct_ciphertexts() -> None: + """Each register call produces a distinct ciphertext (ephemeral key).""" + sk, pk_b64 = _make_nacl_keypair() + resp_lib.add(resp_lib.GET, f"{HOST}/pubkey", json={"key_id": "k", "public_key": pk_b64}) + resp_lib.add(resp_lib.POST, f"{HOST}/register/encrypted", json={"uuid": GOLDEN_UUID, "status": None}) + resp_lib.add(resp_lib.GET, f"{HOST}/pubkey", json={"key_id": "k", "public_key": pk_b64}) + resp_lib.add(resp_lib.POST, f"{HOST}/register/encrypted", json={"uuid": GOLDEN_UUID, "status": None}) + + scanner = _make_scanner() + vk = _golden_view_key() + scanner.register_encrypted(vk, 0) + scanner.register_encrypted(vk, 0) + + ct1 = json.loads(resp_lib.calls[1].request.body)["ciphertext"] + ct2 = json.loads(resp_lib.calls[3].request.body)["ciphertext"] + assert ct1 != ct2 + + +# --------------------------------------------------------------------------- +# 6. revoke — body is quoted JSON string +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_revoke_body_is_quoted_string() -> None: + """revoke() POSTs a quoted JSON string body like '"uuid"'.""" + resp_lib.add(resp_lib.POST, f"{HOST}/revoke", json={"status": "OK"}) + + scanner = _make_scanner() + scanner._uuid = None + # Manually set UUID using a real field + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + result = scanner.revoke() + + assert result["ok"] is True + assert result["data"]["status"] == "OK" + + post_body = resp_lib.calls[0].request.body + # Body should be the uuid string quoted as JSON + assert post_body == json.dumps(GOLDEN_UUID) + assert post_body == f'"{GOLDEN_UUID}"' + + +# --------------------------------------------------------------------------- +# 7. revoke — no UUID raises UUIDError (no HTTP call) +# --------------------------------------------------------------------------- + +def test_revoke_no_uuid_raises() -> None: + scanner = _make_scanner() + with pytest.raises(UUIDError): + scanner.revoke() + + +# --------------------------------------------------------------------------- +# 8. revoke — invalid UUID raises UUIDError (no HTTP call) +# --------------------------------------------------------------------------- + +def test_revoke_invalid_uuid_raises() -> None: + scanner = _make_scanner() + with pytest.raises(UUIDError) as exc_info: + scanner.revoke("not-a-valid-field") + assert exc_info.value.uuid == "not-a-valid-field" + + +# --------------------------------------------------------------------------- +# 9. status — body and shape +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_status_body_and_shape() -> None: + resp_lib.add( + resp_lib.POST, + f"{HOST}/status", + json={"synced": True, "percentage": 100.0}, + ) + + scanner = _make_scanner() + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + result = scanner.status() + + assert result["ok"] is True + assert result["data"]["synced"] is True + assert result["data"]["percentage"] == 100.0 + + # Body is quoted uuid string + body = resp_lib.calls[0].request.body + assert body == json.dumps(GOLDEN_UUID) + + +# --------------------------------------------------------------------------- +# 10. owned — UUID resolution and filter mutation +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_owned_uuid_resolution_and_mutation() -> None: + """Filter with invalid uuid → scanner's uuid used; filter mutated.""" + resp_lib.add(resp_lib.POST, f"{HOST}/records/owned", json=[]) + + scanner = _make_scanner() + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + filter_dict: dict[str, Any] = {"uuid": "invalid-uuid", "unspent": True} + result = scanner.owned(filter_dict) # type: ignore[arg-type] + + assert result["ok"] is True + # Filter was mutated to valid uuid + assert filter_dict["uuid"] == GOLDEN_UUID + + # Body == json.dumps(filter) after mutation + body = resp_lib.calls[0].request.body + assert json.loads(body) == filter_dict + + +# --------------------------------------------------------------------------- +# 11. owned — verbatim body with complex OwnedFilter +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_owned_verbatim_body() -> None: + """Complex OwnedFilter → body == json.dumps(filter) after uuid mutation.""" + resp_lib.add(resp_lib.POST, f"{HOST}/records/owned", json=[]) + + scanner = _make_scanner() + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + filter_dict: dict[str, Any] = { + "uuid": "test-uuid", # invalid — scanner's uuid will be used + "unspent": True, + "filter": {"program": "credits.aleo", "record": "credits"}, + "responseFilter": {"record_ciphertext": True, "spent": True}, + } + + result = scanner.owned(filter_dict) # type: ignore[arg-type] + assert result["ok"] is True + + # After owned(), filter["uuid"] == GOLDEN_UUID + assert filter_dict["uuid"] == GOLDEN_UUID + + body = json.loads(resp_lib.calls[0].request.body) + assert body == filter_dict + + +# --------------------------------------------------------------------------- +# 12. owned — 422 auto re-register +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_owned_422_auto_reregister() -> None: + """On 422 with auto_re_register=True + stored vk → re-register and retry.""" + from aleo.mainnet import Field + sk, pk_b64 = _make_nacl_keypair() + + # First owned call → 422 + resp_lib.add(resp_lib.POST, f"{HOST}/records/owned", status=422, json={}) + # pubkey for re-registration + resp_lib.add(resp_lib.GET, f"{HOST}/pubkey", json={"key_id": "k", "public_key": pk_b64}) + # register/encrypted + resp_lib.add(resp_lib.POST, f"{HOST}/register/encrypted", json={"uuid": GOLDEN_UUID, "status": None}) + # Second owned call → 200 + resp_lib.add(resp_lib.POST, f"{HOST}/records/owned", json=OWNED_RECORDS) + + scanner = _make_scanner(auto_re_register=True) + vk = _golden_view_key() + scanner._uuid = Field.from_string(GOLDEN_UUID) + scanner._view_keys[GOLDEN_UUID] = vk + + filter_dict: dict[str, Any] = {"uuid": GOLDEN_UUID} + result = scanner.owned(filter_dict) # type: ignore[arg-type] + + assert result["ok"] is True + assert len(result["data"]) == 2 + + # Verify register was called (pubkey + register/encrypted calls exist) + urls = [c.request.url for c in resp_lib.calls] + assert any("/pubkey" in u for u in urls) + assert any("/register/encrypted" in u for u in urls) + + +# --------------------------------------------------------------------------- +# 13. encrypted_records — filter passthrough +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_encrypted_records_filter_passthrough() -> None: + records_filter = {"programs": ["credits.aleo"], "start": 0, "end": 100} + resp_lib.add(resp_lib.POST, f"{HOST}/records/encrypted", json=[{"record_ciphertext": "ct1"}]) + + scanner = _make_scanner() + result = scanner.encrypted(records_filter) # type: ignore[arg-type] + + assert result["ok"] is True + assert result["data"][0]["record_ciphertext"] == "ct1" + + assert "/records/encrypted" in resp_lib.calls[0].request.url + body = json.loads(resp_lib.calls[0].request.body) + assert body == records_filter + + +# --------------------------------------------------------------------------- +# 14. check_serial_numbers +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_check_serial_numbers() -> None: + resp_lib.add(resp_lib.POST, f"{HOST}/records/sns", json=CHECK_SNS_RESPONSE) + + scanner = _make_scanner() + sns = list(CHECK_SNS_RESPONSE.keys()) + result = scanner.check_serial_numbers(sns) + + assert result["ok"] is True + assert result["data"] == CHECK_SNS_RESPONSE + assert "/records/sns" in resp_lib.calls[0].request.url + assert json.loads(resp_lib.calls[0].request.body) == sns + + +# --------------------------------------------------------------------------- +# 15. check_tags +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_check_tags() -> None: + resp_lib.add(resp_lib.POST, f"{HOST}/records/tags", json=CHECK_TAGS_RESPONSE) + + scanner = _make_scanner() + tags = list(CHECK_TAGS_RESPONSE.keys()) + result = scanner.check_tags(tags) + + assert result["ok"] is True + assert result["data"] == CHECK_TAGS_RESPONSE + assert "/records/tags" in resp_lib.calls[0].request.url + assert json.loads(resp_lib.calls[0].request.body) == tags + + +# --------------------------------------------------------------------------- +# 16. compute_uuid — golden KAT +# --------------------------------------------------------------------------- + +def test_compute_uuid_golden_kat() -> None: + from aleo.mainnet import PrivateKey + pk = PrivateKey.from_string(GOLDEN_PRIVATE_KEY) + vk = pk.view_key + uuid_field = compute_uuid(vk) + assert str(uuid_field) == GOLDEN_UUID + + +# --------------------------------------------------------------------------- +# 17. uuid_is_valid +# --------------------------------------------------------------------------- + +def test_uuid_is_valid_true() -> None: + assert uuid_is_valid(GOLDEN_UUID) is True + + +def test_uuid_is_valid_false() -> None: + assert uuid_is_valid("not-a-uuid") is False + assert uuid_is_valid("") is False + + +# --------------------------------------------------------------------------- +# 18. JWT origin table +# --------------------------------------------------------------------------- + +def test_jwt_origin_table() -> None: + """JWT refresh goes to scheme+host (no path).""" + from aleo._client_common import jwt_origin + cases = [ + ("https://api.provable.com/v2", "https://api.provable.com"), + ("https://record-scanner.aleo.org/mainnet", "https://record-scanner.aleo.org"), + ("http://localhost:8080/some/path", "http://localhost:8080"), + ("https://scanner.example.com", "https://scanner.example.com"), + ] + for url, expected in cases: + assert jwt_origin(url) == expected, f"jwt_origin({url!r}) != {expected!r}" + + +@resp_lib.activate +def test_jwt_origin_used_for_jwt_refresh() -> None: + """JWT POST goes to origin/jwts/{consumer_id}, not the scanner url.""" + jwt_url = "https://record-scanner.aleo.org/jwts/consumer123" + resp_lib.add( + resp_lib.POST, + jwt_url, + json={"exp": 9999999999}, + headers={"Authorization": "Bearer test-jwt"}, + ) + resp_lib.add(resp_lib.POST, f"{HOST}/revoke", json={"status": "OK"}) + + scanner = _make_scanner( + api_key="apikey", + consumer_id="consumer123", + ) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + # Force JWT refresh by leaving jwt_data as None + scanner.jwt_data = None + + result = scanner.revoke() + assert result["ok"] is True + + jwt_call = resp_lib.calls[0] + assert "jwts/consumer123" in jwt_call.request.url + assert "record-scanner.aleo.org" in jwt_call.request.url + # Must NOT include /mainnet path + assert "/mainnet" not in jwt_call.request.url + + +# --------------------------------------------------------------------------- +# 19. find_credits_record — decrypt_not_enabled_error +# --------------------------------------------------------------------------- + +def test_find_credits_record_decrypt_not_enabled_error() -> None: + scanner = _make_scanner(decrypt_enabled=False) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + with pytest.raises(DecryptionNotEnabledError): + scanner.find_credits_record(1_000_000, {}) # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# 20. find_credits_record — view_key_not_stored_error +# --------------------------------------------------------------------------- + +def test_find_credits_record_view_key_not_stored_error() -> None: + scanner = _make_scanner(decrypt_enabled=True) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + # No view key stored + + with pytest.raises(ViewKeyNotStoredError) as exc_info: + scanner.find_credits_record(1_000_000, {}) # type: ignore[arg-type] + assert exc_info.value.uuid == GOLDEN_UUID + + +# --------------------------------------------------------------------------- +# 21. find_credits_record — success +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_find_credits_record_success() -> None: + """find_credits_record returns first record with >= microcredits.""" + resp_lib.add(resp_lib.POST, f"{HOST}/records/owned", json=OWNED_CREDITS_RECORDS) + + scanner = _make_scanner(decrypt_enabled=True) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + vk = _golden_view_key() + scanner._view_keys[GOLDEN_UUID] = vk + + # RECORD_PLAINTEXT_STR has 1_500_000_000_000_000 microcredits + result = scanner.find_credits_record(1_000_000, {}) # type: ignore[arg-type] + assert "record_plaintext" in result + + +# --------------------------------------------------------------------------- +# 22. decrypt in-place +# --------------------------------------------------------------------------- + +def test_decrypt_in_place() -> None: + from aleo.mainnet import ViewKey + vk = ViewKey.from_string(VIEW_KEY_STRING) + scanner = _make_scanner() + + records: list[dict[str, Any]] = [ + {"record_ciphertext": RECORD_CIPHERTEXT_STRING}, + {"record_ciphertext": ""}, # empty — should be skipped + {"record_ciphertext": "invalid-ct"}, # invalid — silently skipped + ] + scanner.decrypt(vk, records) + + # First record decrypted + assert "record_plaintext" in records[0] + assert "microcredits" in records[0]["record_plaintext"] + # Second record: no plaintext set (empty ciphertext) + assert "record_plaintext" not in records[1] + # Third record: silently skipped (bad ciphertext) + assert "record_plaintext" not in records[2] + + +# --------------------------------------------------------------------------- +# 23. find_record — returns first +# --------------------------------------------------------------------------- + +@resp_lib.activate +def test_find_record_returns_first() -> None: + resp_lib.add(resp_lib.POST, f"{HOST}/records/owned", json=OWNED_RECORDS) + + scanner = _make_scanner() + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + record = scanner.find_record({"uuid": GOLDEN_UUID}) # type: ignore[arg-type] + assert record == OWNED_RECORDS[0] diff --git a/sdk/python/tests/test_record_scanner_async.py b/sdk/python/tests/test_record_scanner_async.py new file mode 100644 index 0000000..31b8052 --- /dev/null +++ b/sdk/python/tests/test_record_scanner_async.py @@ -0,0 +1,338 @@ +"""Tests for AsyncRecordScanner (async, httpx-based). All mocked — no live network.""" +from __future__ import annotations + +import base64 +import json +from typing import Any, Callable + +import httpx +import pytest + +from aleo.async_record_scanner import AsyncRecordScanner +from aleo._scanner_common import ( + compute_uuid, + DecryptionNotEnabledError, + RecordScannerRequestError, + UUIDError, + ViewKeyNotStoredError, +) + +pytestmark = pytest.mark.asyncio + +# --------------------------------------------------------------------------- +# KAT constants +# --------------------------------------------------------------------------- + +GOLDEN_PRIVATE_KEY = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" +GOLDEN_UUID = "7884164224800444110633570141944665301008802280502652120359195870264061098703field" +VIEW_KEY_STRING = "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX" +RECORD_CIPHERTEXT_STRING = ( + "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzq" + "rtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5" + "pxm5cmxsv4un97q" +) + +CHECK_SNS_RESPONSE: dict[str, bool] = { + "1621694306596217216370326054181178914897851479837084979111511176605457690717field": True, + "5684626152578699086223993752521225507576791345254401210560771329591763880242field": False, +} + +CHECK_TAGS_RESPONSE: dict[str, bool] = { + "2965517500209150226508265073635793457193572667031485750956287906078711930968field": False, + "8421937347379608036510120951995833971195343843566214313082589116311107280540field": False, +} + +BASE_URL = "https://record-scanner.aleo.org" +HOST = f"{BASE_URL}/mainnet" + +RECORD_PLAINTEXT_STR = ( + "{\n owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,\n" + " microcredits: 1500000000000000u64.private,\n" + " _nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public,\n" + " _version: 0u8.public\n}" +) + +OWNED_CREDITS_RECORDS = [ + { + "record_ciphertext": RECORD_CIPHERTEXT_STRING, + "record_plaintext": RECORD_PLAINTEXT_STR, + "program_name": "credits.aleo", + "record_name": "credits", + "spent": False, + }, +] + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _golden_view_key() -> Any: + from aleo.mainnet import PrivateKey + return PrivateKey.from_string(GOLDEN_PRIVATE_KEY).view_key + + +def _make_nacl_keypair() -> tuple[Any, str]: + from nacl.public import PrivateKey as NaclPrivateKey + sk = NaclPrivateKey.generate() + pk_b64 = base64.b64encode(bytes(sk.public_key)).decode() + return sk, pk_b64 + + +def route_handler( + routes: dict[str, Any], +) -> Callable[[httpx.Request], httpx.Response]: + """Return a handler for httpx.MockTransport.""" + def handler(request: httpx.Request) -> httpx.Response: + url = str(request.url) + for pattern, response in routes.items(): + if pattern in url: + if callable(response): + return response(request) + return response + return httpx.Response(404, text="not found") + return handler + + +def jr(data: Any, status: int = 200, headers: dict[str, str] | None = None) -> httpx.Response: + return httpx.Response(status, json=data, headers=headers or {}) + + +def _make_scanner( + routes: dict[str, Any] | None = None, + **kwargs: Any, +) -> AsyncRecordScanner: + transport = httpx.MockTransport(route_handler(routes or {})) + return AsyncRecordScanner(BASE_URL, transport=transport, **kwargs) + + +# --------------------------------------------------------------------------- +# 1. URL construction +# --------------------------------------------------------------------------- + +async def test_async_url_construction() -> None: + scanner = AsyncRecordScanner("https://record-scanner.aleo.org") + assert scanner.url == "https://record-scanner.aleo.org/mainnet" + + +# --------------------------------------------------------------------------- +# 2. Trailing network raises ValueError +# --------------------------------------------------------------------------- + +async def test_async_trailing_network_raises() -> None: + with pytest.raises(ValueError, match="should not include"): + AsyncRecordScanner("https://record-scanner.aleo.org/mainnet") + + with pytest.raises(ValueError, match="should not include"): + AsyncRecordScanner("https://record-scanner.aleo.org/testnet") + + +# --------------------------------------------------------------------------- +# 3. register_encrypted flow +# --------------------------------------------------------------------------- + +async def test_async_register_encrypted_flow() -> None: + """Register flow: GET /pubkey, POST /register/encrypted, ciphertext decrypts OK.""" + sk, pk_b64 = _make_nacl_keypair() + + routes = { + "/pubkey": jr({"key_id": "k1", "public_key": pk_b64}), + "/register/encrypted": jr({"uuid": GOLDEN_UUID, "status": None}), + } + + scanner = _make_scanner(routes) + vk = _golden_view_key() + result = await scanner.register_encrypted(vk, 0) + + assert result["ok"] is True + assert result["data"]["uuid"] == GOLDEN_UUID + assert scanner._uuid is not None + assert str(scanner._uuid) == GOLDEN_UUID + + +async def test_async_register_encrypted_wire_shape() -> None: + """Ciphertext in POST body decrypts to vk_bytes + LE u32.""" + sk, pk_b64 = _make_nacl_keypair() + captured: list[httpx.Request] = [] + + def capture_register(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr({"uuid": GOLDEN_UUID, "status": None}) + + routes = { + "/pubkey": jr({"key_id": "k", "public_key": pk_b64}), + "/register/encrypted": capture_register, + } + + scanner = _make_scanner(routes) + vk = _golden_view_key() + await scanner.register_encrypted(vk, 42) + + assert len(captured) == 1 + body = json.loads(captured[0].content) + raw = base64.b64decode(body["ciphertext"]) + + # Wire shape: 84 bytes + assert len(raw) == 84 + + from nacl.public import SealedBox + import struct + plaintext = SealedBox(sk).decrypt(raw) + assert len(plaintext) == 36 + assert plaintext[:32] == bytes(vk.bytes()) + assert plaintext[32:] == struct.pack("<I", 42) + + +# --------------------------------------------------------------------------- +# 4. revoke — body is quoted JSON string +# --------------------------------------------------------------------------- + +async def test_async_revoke_body_is_quoted_string() -> None: + captured: list[httpx.Request] = [] + + def capture_revoke(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr({"status": "OK"}) + + routes = {"/revoke": capture_revoke} + scanner = _make_scanner(routes) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + result = await scanner.revoke() + assert result["ok"] is True + + body = captured[0].content.decode() + assert body == json.dumps(GOLDEN_UUID) + assert body == f'"{GOLDEN_UUID}"' + + +# --------------------------------------------------------------------------- +# 5. revoke — no UUID raises UUIDError +# --------------------------------------------------------------------------- + +async def test_async_revoke_no_uuid_raises() -> None: + scanner = _make_scanner() + with pytest.raises(UUIDError): + await scanner.revoke() + + +# --------------------------------------------------------------------------- +# 6. owned — UUID mutation +# --------------------------------------------------------------------------- + +async def test_async_owned_uuid_mutation() -> None: + """Filter with invalid uuid → scanner uuid used; filter mutated.""" + captured: list[httpx.Request] = [] + + def capture_owned(req: httpx.Request) -> httpx.Response: + captured.append(req) + return jr([]) + + routes = {"/records/owned": capture_owned} + scanner = _make_scanner(routes) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + filter_dict: dict[str, Any] = {"uuid": "invalid-uuid", "unspent": True} + result = await scanner.owned(filter_dict) # type: ignore[arg-type] + + assert result["ok"] is True + assert filter_dict["uuid"] == GOLDEN_UUID + + body = json.loads(captured[0].content) + assert body == filter_dict + + +# --------------------------------------------------------------------------- +# 7. compute_uuid — golden KAT +# --------------------------------------------------------------------------- + +async def test_async_compute_uuid_golden_kat() -> None: + from aleo.mainnet import PrivateKey + pk = PrivateKey.from_string(GOLDEN_PRIVATE_KEY) + vk = pk.view_key + uuid_field = compute_uuid(vk) + assert str(uuid_field) == GOLDEN_UUID + + +# --------------------------------------------------------------------------- +# 8. check_serial_numbers +# --------------------------------------------------------------------------- + +async def test_async_check_serial_numbers() -> None: + routes = {"/records/sns": jr(CHECK_SNS_RESPONSE)} + scanner = _make_scanner(routes) + sns = list(CHECK_SNS_RESPONSE.keys()) + result = await scanner.check_serial_numbers(sns) + + assert result["ok"] is True + assert result["data"] == CHECK_SNS_RESPONSE + + +# --------------------------------------------------------------------------- +# 9. check_tags +# --------------------------------------------------------------------------- + +async def test_async_check_tags() -> None: + routes = {"/records/tags": jr(CHECK_TAGS_RESPONSE)} + scanner = _make_scanner(routes) + tags = list(CHECK_TAGS_RESPONSE.keys()) + result = await scanner.check_tags(tags) + + assert result["ok"] is True + assert result["data"] == CHECK_TAGS_RESPONSE + + +# --------------------------------------------------------------------------- +# 10. find_credits_record — decrypt_not_enabled_error +# --------------------------------------------------------------------------- + +async def test_async_find_credits_record_decrypt_not_enabled_error() -> None: + scanner = _make_scanner(decrypt_enabled=False) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + with pytest.raises(DecryptionNotEnabledError): + await scanner.find_credits_record(1_000_000, {}) # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# Extra: find_credits_record view key not stored +# --------------------------------------------------------------------------- + +async def test_async_find_credits_record_view_key_not_stored() -> None: + scanner = _make_scanner(decrypt_enabled=True) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + with pytest.raises(ViewKeyNotStoredError): + await scanner.find_credits_record(1_000_000, {}) # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# Extra: revoke invalid UUID raises UUIDError +# --------------------------------------------------------------------------- + +async def test_async_revoke_invalid_uuid_raises() -> None: + scanner = _make_scanner() + with pytest.raises(UUIDError) as exc_info: + await scanner.revoke("not-a-valid-field") + assert exc_info.value.uuid == "not-a-valid-field" + + +# --------------------------------------------------------------------------- +# Extra: find_record returns first +# --------------------------------------------------------------------------- + +async def test_async_find_record_returns_first() -> None: + owned_records = [ + {"commitment": "cm1abc", "record_ciphertext": RECORD_CIPHERTEXT_STRING}, + {"commitment": "cm1def", "record_ciphertext": RECORD_CIPHERTEXT_STRING}, + ] + routes = {"/records/owned": jr(owned_records)} + scanner = _make_scanner(routes) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + record = await scanner.find_record({"uuid": GOLDEN_UUID}) # type: ignore[arg-type] + assert record == owned_records[0] From 3cd88109afb2a991360913ad47943500b69f36ea Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 15:06:32 -0400 Subject: [PATCH 64/72] fix: add find_credits_records to AsyncRecordScanner for sync/async parity Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/async_record_scanner.py | 58 ++++++++++++ sdk/python/tests/test_record_scanner_async.py | 93 +++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/sdk/python/aleo/async_record_scanner.py b/sdk/python/aleo/async_record_scanner.py index 3982881..6043715 100644 --- a/sdk/python/aleo/async_record_scanner.py +++ b/sdk/python/aleo/async_record_scanner.py @@ -491,3 +491,61 @@ async def find_credits_record( raise RecordNotFoundError( f"No credits record found with >= {microcredits} microcredits." ) + + async def find_credits_records( + self, microcredit_amounts: list[int], filter: OwnedFilter + ) -> list[OwnedRecord]: + """Find credits.aleo records whose microcredits are in microcredit_amounts. + + Requires decrypt_enabled=True and a stored view key for the UUID. + """ + # Resolve UUID + filter_uuid = filter.get("uuid", "") + if filter_uuid and uuid_is_valid(filter_uuid): + resolved_uuid = filter_uuid + elif self._uuid is not None: + resolved_uuid = str(self._uuid) + else: + raise UUIDError("No UUID configured.") + + if not self.decrypt_enabled: + raise DecryptionNotEnabledError( + "decrypt_enabled must be True to use find_credits_records." + ) + + vk = self._view_keys.get(resolved_uuid) + if vk is None: + raise ViewKeyNotStoredError( + f"No view key stored for uuid '{resolved_uuid}'.", + uuid=resolved_uuid, + ) + + credits_filter: OwnedFilter = { + "unspent": filter.get("unspent", True), # type: ignore[typeddict-item] + "filter": { # type: ignore[typeddict-item] + "program": "credits.aleo", + "record": "credits", + **filter.get("filter", {}), # type: ignore[arg-type] + }, + "uuid": resolved_uuid, + } + if "responseFilter" in filter: + credits_filter["responseFilter"] = filter["responseFilter"] # type: ignore[typeddict-item] + + records = await self.find_records(credits_filter) + + from .mainnet import RecordPlaintext # type: ignore[attr-defined] + amounts_set = set(microcredit_amounts) + result: list[OwnedRecord] = [] + for record in records: + pt_str = record.get("record_plaintext", "") + if not pt_str: + continue + try: + pt = RecordPlaintext.from_string(pt_str) + if pt.microcredits in amounts_set: + result.append(record) + except Exception: + pass + + return result diff --git a/sdk/python/tests/test_record_scanner_async.py b/sdk/python/tests/test_record_scanner_async.py index 31b8052..d9dd6ea 100644 --- a/sdk/python/tests/test_record_scanner_async.py +++ b/sdk/python/tests/test_record_scanner_async.py @@ -62,6 +62,36 @@ }, ] +# Two credits records with plaintext bearing distinct microcredit amounts +_RECORD_PLAINTEXT_1M = ( + "{\n owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,\n" + " microcredits: 1000000u64.private,\n" + " _nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public,\n" + " _version: 0u8.public\n}" +) +_RECORD_PLAINTEXT_2M = ( + "{\n owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,\n" + " microcredits: 2000000u64.private,\n" + " _nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public,\n" + " _version: 0u8.public\n}" +) +OWNED_CREDITS_RECORDS_MULTI = [ + { + "record_ciphertext": RECORD_CIPHERTEXT_STRING, + "record_plaintext": _RECORD_PLAINTEXT_1M, + "program_name": "credits.aleo", + "record_name": "credits", + "spent": False, + }, + { + "record_ciphertext": RECORD_CIPHERTEXT_STRING, + "record_plaintext": _RECORD_PLAINTEXT_2M, + "program_name": "credits.aleo", + "record_name": "credits", + "spent": False, + }, +] + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -336,3 +366,66 @@ async def test_async_find_record_returns_first() -> None: record = await scanner.find_record({"uuid": GOLDEN_UUID}) # type: ignore[arg-type] assert record == owned_records[0] + + +# --------------------------------------------------------------------------- +# Extra: find_credits_records — decrypt not enabled +# --------------------------------------------------------------------------- + +async def test_async_find_credits_records_decrypt_not_enabled() -> None: + scanner = _make_scanner(decrypt_enabled=False) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + with pytest.raises(DecryptionNotEnabledError): + await scanner.find_credits_records([1_000_000], {}) # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# Extra: find_credits_records — view key not stored +# --------------------------------------------------------------------------- + +async def test_async_find_credits_records_view_key_not_stored() -> None: + scanner = _make_scanner(decrypt_enabled=True) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + + with pytest.raises(ViewKeyNotStoredError): + await scanner.find_credits_records([1_000_000], {}) # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# Extra: find_credits_records — success path exact-match filtering +# --------------------------------------------------------------------------- + +async def test_async_find_credits_records_success() -> None: + """find_credits_records returns only records whose microcredits are in the amounts list.""" + routes = {"/records/owned": jr(OWNED_CREDITS_RECORDS_MULTI)} + scanner = _make_scanner(routes, decrypt_enabled=True) + from aleo.mainnet import Field + scanner._uuid = Field.from_string(GOLDEN_UUID) + vk = _golden_view_key() + scanner._view_keys[GOLDEN_UUID] = vk + + # Request only 1_000_000 — should return exactly the first record + result = await scanner.find_credits_records([1_000_000], {}) # type: ignore[arg-type] + assert len(result) == 1 + assert result[0]["record_plaintext"] == _RECORD_PLAINTEXT_1M + + # Request both amounts — both records returned + routes2 = {"/records/owned": jr(OWNED_CREDITS_RECORDS_MULTI)} + scanner2 = _make_scanner(routes2, decrypt_enabled=True) + scanner2._uuid = Field.from_string(GOLDEN_UUID) + scanner2._view_keys[GOLDEN_UUID] = vk + + result2 = await scanner2.find_credits_records([1_000_000, 2_000_000], {}) # type: ignore[arg-type] + assert len(result2) == 2 + + # Request an amount not present — empty list returned + routes3 = {"/records/owned": jr(OWNED_CREDITS_RECORDS_MULTI)} + scanner3 = _make_scanner(routes3, decrypt_enabled=True) + scanner3._uuid = Field.from_string(GOLDEN_UUID) + scanner3._view_keys[GOLDEN_UUID] = vk + + result3 = await scanner3.find_credits_records([999], {}) # type: ignore[arg-type] + assert result3 == [] From f5ecba11aa7945cb748549b486b2bf20f27a943f Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 15:24:34 -0400 Subject: [PATCH 65/72] =?UTF-8?q?feat:=20aleo-abi=20package=20=E2=80=94=20?= =?UTF-8?q?contract=20ABI=20generation=20from=20Leo's=20abi=20crate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds sdk-abi/, a standalone maturin/PyO3 package (aleo-abi) that wraps leo-abi@ba2c017 to expose generate_abi() and check_compatibility() to Python. Also wires a lazy aleo.abi integration module into the main SDK and extends sdk-wheels.yml with build-abi / sdist-abi jobs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .github/workflows/sdk-wheels.yml | 76 +- sdk-abi/.gitignore | 10 + sdk-abi/Cargo.lock | 4179 +++++++++++++++++++++++++ sdk-abi/Cargo.toml | 22 + sdk-abi/pyproject.toml | 15 + sdk-abi/python/aleo_abi/__init__.py | 7 + sdk-abi/python/aleo_abi/_aleo_abi.pyi | 2 + sdk-abi/python/aleo_abi/py.typed | 0 sdk-abi/python/tests/test_abi.py | 238 ++ sdk-abi/src/lib.rs | 68 + sdk/python/aleo/__init__.py | 10 + sdk/python/aleo/abi.py | 91 + sdk/python/tests/test_abi_hook.py | 73 + 13 files changed, 4789 insertions(+), 2 deletions(-) create mode 100644 sdk-abi/.gitignore create mode 100644 sdk-abi/Cargo.lock create mode 100644 sdk-abi/Cargo.toml create mode 100644 sdk-abi/pyproject.toml create mode 100644 sdk-abi/python/aleo_abi/__init__.py create mode 100644 sdk-abi/python/aleo_abi/_aleo_abi.pyi create mode 100644 sdk-abi/python/aleo_abi/py.typed create mode 100644 sdk-abi/python/tests/test_abi.py create mode 100644 sdk-abi/src/lib.rs create mode 100644 sdk/python/aleo/abi.py create mode 100644 sdk/python/tests/test_abi_hook.py diff --git a/.github/workflows/sdk-wheels.yml b/.github/workflows/sdk-wheels.yml index 2487d03..fa53c8c 100644 --- a/.github/workflows/sdk-wheels.yml +++ b/.github/workflows/sdk-wheels.yml @@ -4,6 +4,7 @@ on: push: paths: - 'sdk/**' + - 'sdk-abi/**' - '.github/workflows/sdk-wheels.yml' - '.github/workflows/sdk.yml' branches: @@ -14,6 +15,7 @@ on: pull_request: paths: - 'sdk/**' + - 'sdk-abi/**' - '.github/workflows/sdk-wheels.yml' - '.github/workflows/sdk.yml' workflow_dispatch: @@ -95,6 +97,59 @@ jobs: name: wheels-${{ matrix.platform.name }} path: sdk/dist + build-abi: + name: abi-build (${{ matrix.platform.name }}) + needs: [] + runs-on: ${{ matrix.platform.runner }} + strategy: + fail-fast: false + matrix: + platform: + - name: linux-x86_64 + runner: ubuntu-latest + target: x86_64 + - name: linux-aarch64 + runner: ubuntu-24.04-arm + target: aarch64 + - name: macos-x86_64 + runner: macos-13 + target: x86_64 + - name: macos-aarch64 + runner: macos-latest + target: aarch64 + - name: windows-x64 + runner: windows-latest + target: x64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Build wheel + uses: PyO3/maturin-action@v1 + with: + working-directory: ./sdk-abi + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: 'true' + manylinux: ${{ startsWith(matrix.platform.name, 'linux') && '2_28' || 'auto' }} + before-script-linux: | + if command -v dnf &> /dev/null; then + dnf install -y git cmake clang + elif command -v yum &> /dev/null; then + yum install -y git cmake3 clang && ln -sf "$(command -v cmake3)" /usr/local/bin/cmake + fi + - name: Smoke test wheel + shell: bash + run: | + pip install --no-index --find-links sdk-abi/dist aleo-abi + python -c "import aleo_abi; print(aleo_abi.generate_abi.__name__)" + - name: Upload wheel + uses: actions/upload-artifact@v4 + with: + name: abi-wheels-${{ matrix.platform.name }} + path: sdk-abi/dist + sdist: needs: [tests] runs-on: ubuntu-latest @@ -112,15 +167,32 @@ jobs: name: wheels-sdist path: sdk/dist + sdist-abi: + needs: [] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + working-directory: ./sdk-abi + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: abi-wheels-sdist + path: sdk-abi/dist + release: name: Release runs-on: ubuntu-latest if: "startsWith(github.ref, 'refs/tags/')" - needs: [build, sdist] + needs: [build, sdist, build-abi, sdist-abi] steps: - uses: actions/download-artifact@v4 with: - pattern: wheels-* + pattern: '*wheels-*' merge-multiple: true path: dist - name: Publish to PyPI diff --git a/sdk-abi/.gitignore b/sdk-abi/.gitignore new file mode 100644 index 0000000..ab2d913 --- /dev/null +++ b/sdk-abi/.gitignore @@ -0,0 +1,10 @@ +target/ +.env/ +dist/ +__pycache__/ +*.pyc +*.pyo +*.so +*.pyd +*.whl +.pytest_cache/ diff --git a/sdk-abi/Cargo.lock b/sdk-abi/Cargo.lock new file mode 100644 index 0000000..bc5e2b7 --- /dev/null +++ b/sdk-abi/Cargo.lock @@ -0,0 +1,4179 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aleo-abi" +version = "0.1.0" +dependencies = [ + "anyhow", + "leo-abi", + "leo-ast", + "pyo3", + "serde_json", +] + +[[package]] +name = "aleo-std" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869be259eeb00852087ad36e9a68c67959c2811dc8fc6c680fcac98948c4f172" +dependencies = [ + "aleo-std-cpu", + "aleo-std-profiler", + "aleo-std-storage", + "aleo-std-time", + "aleo-std-timed", + "aleo-std-timer", + "walkdir", +] + +[[package]] +name = "aleo-std-cpu" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9881417111e9266cf47bb2fec0e7ef32454fa2ac05763a48f25c1d50f260e1b9" + +[[package]] +name = "aleo-std-profiler" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d53500befe6ecd23f8fc11ccf510b680516b3a6aff08d60f12e1dda00b77787" + +[[package]] +name = "aleo-std-storage" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f6571ae16ee20c1e9a95b6ba685ca7156d519400f597c5e59f53b641637225" +dependencies = [ + "dirs", + "tempfile", +] + +[[package]] +name = "aleo-std-time" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ebd144c81671193ed85aa2db9bb5e183421843e0485de8fffc07e5cf50e18a" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "aleo-std-timed" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f6ff9e4c36858fa2c29e5284b77527b5a7466743976e1ba1f5824e16683545" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "aleo-std-timer" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12aca1021aef2c476bad30d2f681e891b2be4f07dbc230a96df09cb693bfb3cb" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "ariadne" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8454c8a44ce2cb9cc7e7fae67fc6128465b343b92c6631e94beca3c8d1524ea5" +dependencies = [ + "unicode-width", + "yansi", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "color-backtrace" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308329d5d62e877ba02943db3a8e8c052de9fde7ab48283395ba0e6494efbabd" +dependencies = [ + "backtrace", + "termcolor", +] + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest 0.11.3", + "elliptic-curve", + "rfc6979", + "signature", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff", + "group", + "hybrid-array", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "enum_index" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5532bdea562e7be83060c36185eecccba82fe16729d2eaad2891d65417656dd" + +[[package]] +name = "enum_index_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab22c8085548bf06190113dca556e149ecdbb05ae5b972a2b9899f26b944ee4" +dependencies = [ + "quote 0.3.15", + "syn 0.11.11", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "rayon", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" +dependencies = [ + "cpubits", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", + "wnaf", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leo-abi" +version = "4.3.2" +source = "git+https://github.com/ProvableHQ/leo?rev=ba2c01722a48f84b4d90b93aeaf8305b7c03dbce#ba2c01722a48f84b4d90b93aeaf8305b7c03dbce" +dependencies = [ + "indexmap", + "itertools", + "leo-abi-types", + "leo-ast", + "leo-disassembler", + "leo-span", +] + +[[package]] +name = "leo-abi-types" +version = "4.3.2" +source = "git+https://github.com/ProvableHQ/leo?rev=ba2c01722a48f84b4d90b93aeaf8305b7c03dbce#ba2c01722a48f84b4d90b93aeaf8305b7c03dbce" +dependencies = [ + "serde", +] + +[[package]] +name = "leo-ast" +version = "4.3.2" +source = "git+https://github.com/ProvableHQ/leo?rev=ba2c01722a48f84b4d90b93aeaf8305b7c03dbce#ba2c01722a48f84b4d90b93aeaf8305b7c03dbce" +dependencies = [ + "anyhow", + "getrandom 0.4.3", + "indexmap", + "itertools", + "leo-errors", + "leo-span", + "serde", + "serde_json", + "snarkvm", + "snarkvm-console", + "snarkvm-synthesizer-program", +] + +[[package]] +name = "leo-disassembler" +version = "4.3.2" +source = "git+https://github.com/ProvableHQ/leo?rev=ba2c01722a48f84b4d90b93aeaf8305b7c03dbce#ba2c01722a48f84b4d90b93aeaf8305b7c03dbce" +dependencies = [ + "getrandom 0.4.3", + "leo-ast", + "leo-errors", + "leo-span", + "snarkvm", + "snarkvm-console", + "snarkvm-synthesizer-program", +] + +[[package]] +name = "leo-errors" +version = "4.3.2" +source = "git+https://github.com/ProvableHQ/leo?rev=ba2c01722a48f84b4d90b93aeaf8305b7c03dbce#ba2c01722a48f84b4d90b93aeaf8305b7c03dbce" +dependencies = [ + "anyhow", + "ariadne", + "backtrace", + "color-backtrace", + "colored", + "derivative", + "itertools", + "leo-span", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "leo-span" +version = "4.3.2" +source = "git+https://github.com/ProvableHQ/leo?rev=ba2c01722a48f84b4d90b93aeaf8305b7c03dbce#ba2c01722a48f84b4d90b93aeaf8305b7c03dbce" +dependencies = [ + "ariadne", + "fxhash", + "indexmap", + "scoped-tls", + "serde", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "primefield", + "wnaf", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "anyhow", + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_xorshift" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "snarkvm" +version = "4.8.1" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "dotenvy", + "rand", + "serde_json", + "snarkvm-algorithms", + "snarkvm-circuit", + "snarkvm-console", + "snarkvm-ledger", + "snarkvm-parameters", + "snarkvm-synthesizer", + "snarkvm-utilities", + "ureq", + "walkdir", +] + +[[package]] +name = "snarkvm-algorithms" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "blake2", + "cfg-if", + "fxhash", + "hashbrown 0.15.5", + "hex", + "indexmap", + "itertools", + "num-traits", + "rand", + "rayon", + "serde", + "sha2", + "smallvec", + "snarkvm-curves", + "snarkvm-fields", + "snarkvm-parameters", + "snarkvm-utilities", + "thiserror 2.0.18", +] + +[[package]] +name = "snarkvm-circuit" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-account", + "snarkvm-circuit-algorithms", + "snarkvm-circuit-collections", + "snarkvm-circuit-environment", + "snarkvm-circuit-network", + "snarkvm-circuit-program", + "snarkvm-circuit-types", +] + +[[package]] +name = "snarkvm-circuit-account" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-network", + "snarkvm-circuit-types", + "snarkvm-console-account", +] + +[[package]] +name = "snarkvm-circuit-algorithms" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-types", + "snarkvm-console-algorithms", + "snarkvm-fields", +] + +[[package]] +name = "snarkvm-circuit-collections" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-algorithms", + "snarkvm-circuit-types", + "snarkvm-console-collections", +] + +[[package]] +name = "snarkvm-circuit-environment" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "indexmap", + "itertools", + "nom", + "num-traits", + "smallvec", + "snarkvm-algorithms", + "snarkvm-circuit-environment-witness", + "snarkvm-console-network", + "snarkvm-curves", + "snarkvm-fields", + "snarkvm-utilities", + "thiserror 2.0.18", +] + +[[package]] +name = "snarkvm-circuit-environment-witness" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" + +[[package]] +name = "snarkvm-circuit-network" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-algorithms", + "snarkvm-circuit-collections", + "snarkvm-circuit-types", + "snarkvm-console-network", +] + +[[package]] +name = "snarkvm-circuit-program" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-account", + "snarkvm-circuit-algorithms", + "snarkvm-circuit-collections", + "snarkvm-circuit-network", + "snarkvm-circuit-types", + "snarkvm-console-program", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-circuit-types" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-circuit-types-address", + "snarkvm-circuit-types-boolean", + "snarkvm-circuit-types-field", + "snarkvm-circuit-types-group", + "snarkvm-circuit-types-integers", + "snarkvm-circuit-types-scalar", + "snarkvm-circuit-types-string", +] + +[[package]] +name = "snarkvm-circuit-types-address" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-circuit-types-boolean", + "snarkvm-circuit-types-field", + "snarkvm-circuit-types-group", + "snarkvm-circuit-types-scalar", + "snarkvm-console-types-address", +] + +[[package]] +name = "snarkvm-circuit-types-boolean" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-console-types-boolean", +] + +[[package]] +name = "snarkvm-circuit-types-field" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-circuit-types-boolean", + "snarkvm-console-types-field", +] + +[[package]] +name = "snarkvm-circuit-types-group" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-circuit-types-boolean", + "snarkvm-circuit-types-field", + "snarkvm-circuit-types-scalar", + "snarkvm-console-types-group", +] + +[[package]] +name = "snarkvm-circuit-types-integers" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-circuit-types-boolean", + "snarkvm-circuit-types-field", + "snarkvm-circuit-types-scalar", + "snarkvm-console-types-integers", +] + +[[package]] +name = "snarkvm-circuit-types-scalar" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-circuit-types-boolean", + "snarkvm-circuit-types-field", + "snarkvm-console-types-scalar", +] + +[[package]] +name = "snarkvm-circuit-types-string" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-circuit-environment", + "snarkvm-circuit-types-boolean", + "snarkvm-circuit-types-field", + "snarkvm-circuit-types-integers", + "snarkvm-console-types-string", +] + +[[package]] +name = "snarkvm-console" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-account", + "snarkvm-console-algorithms", + "snarkvm-console-collections", + "snarkvm-console-network", + "snarkvm-console-program", + "snarkvm-console-types", +] + +[[package]] +name = "snarkvm-console-account" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "bs58", + "snarkvm-console-network", + "snarkvm-console-types", + "zeroize", +] + +[[package]] +name = "snarkvm-console-algorithms" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "blake2s_simd", + "hex", + "k256", + "serde", + "smallvec", + "snarkvm-console-types", + "snarkvm-fields", + "snarkvm-utilities", + "tiny-keccak", +] + +[[package]] +name = "snarkvm-console-collections" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "parking_lot", + "rayon", + "serde", + "snarkvm-console-algorithms", + "snarkvm-console-types", +] + +[[package]] +name = "snarkvm-console-network" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "enum-iterator", + "indexmap", + "lazy_static", + "paste", + "serde", + "snarkvm-algorithms", + "snarkvm-console-algorithms", + "snarkvm-console-collections", + "snarkvm-console-network-environment", + "snarkvm-console-types", + "snarkvm-curves", + "snarkvm-parameters", +] + +[[package]] +name = "snarkvm-console-network-environment" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "bech32", + "itertools", + "nom", + "num-traits", + "rand", + "serde", + "snarkvm-curves", + "snarkvm-fields", + "snarkvm-utilities", + "zeroize", +] + +[[package]] +name = "snarkvm-console-program" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "enum-iterator", + "enum_index", + "enum_index_derive", + "indexmap", + "num-derive", + "num-traits", + "seq-macro", + "serde_json", + "snarkvm-console-account", + "snarkvm-console-algorithms", + "snarkvm-console-collections", + "snarkvm-console-network", + "snarkvm-console-types", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-console-types" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", + "snarkvm-console-types-address", + "snarkvm-console-types-boolean", + "snarkvm-console-types-field", + "snarkvm-console-types-group", + "snarkvm-console-types-integers", + "snarkvm-console-types-scalar", + "snarkvm-console-types-string", +] + +[[package]] +name = "snarkvm-console-types-address" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", + "snarkvm-console-types-boolean", + "snarkvm-console-types-field", + "snarkvm-console-types-group", +] + +[[package]] +name = "snarkvm-console-types-boolean" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", +] + +[[package]] +name = "snarkvm-console-types-field" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", + "snarkvm-console-types-boolean", + "zeroize", +] + +[[package]] +name = "snarkvm-console-types-group" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", + "snarkvm-console-types-boolean", + "snarkvm-console-types-field", + "snarkvm-console-types-scalar", +] + +[[package]] +name = "snarkvm-console-types-integers" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", + "snarkvm-console-types-boolean", + "snarkvm-console-types-field", + "snarkvm-console-types-scalar", +] + +[[package]] +name = "snarkvm-console-types-scalar" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", + "snarkvm-console-types-boolean", + "snarkvm-console-types-field", + "zeroize", +] + +[[package]] +name = "snarkvm-console-types-string" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console-network-environment", + "snarkvm-console-types-boolean", + "snarkvm-console-types-field", + "snarkvm-console-types-integers", +] + +[[package]] +name = "snarkvm-curves" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "rand", + "rustc_version", + "serde", + "snarkvm-fields", + "snarkvm-utilities", + "thiserror 2.0.18", +] + +[[package]] +name = "snarkvm-fields" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "itertools", + "num-traits", + "rand", + "rayon", + "serde", + "snarkvm-utilities", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "snarkvm-ledger" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "cfg-if", + "indexmap", + "lru", + "parking_lot", + "rand", + "rand_chacha", + "rayon", + "snarkvm-console", + "snarkvm-ledger-authority", + "snarkvm-ledger-block", + "snarkvm-ledger-committee", + "snarkvm-ledger-narwhal", + "snarkvm-ledger-puzzle", + "snarkvm-ledger-query", + "snarkvm-ledger-store", + "snarkvm-synthesizer", + "snarkvm-synthesizer-error", + "snarkvm-utilities", + "thiserror 2.0.18", + "time", + "tracing", +] + +[[package]] +name = "snarkvm-ledger-authority" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "rand", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-narwhal-subdag", +] + +[[package]] +name = "snarkvm-ledger-block" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "indexmap", + "itertools", + "rayon", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-authority", + "snarkvm-ledger-committee", + "snarkvm-ledger-narwhal-batch-header", + "snarkvm-ledger-narwhal-data", + "snarkvm-ledger-narwhal-subdag", + "snarkvm-ledger-narwhal-transmission-id", + "snarkvm-ledger-puzzle", + "snarkvm-synthesizer-error", + "snarkvm-synthesizer-program", + "snarkvm-synthesizer-snark", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-ledger-committee" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "indexmap", + "rayon", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-narwhal-batch-header", +] + +[[package]] +name = "snarkvm-ledger-narwhal" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-ledger-narwhal-batch-certificate", + "snarkvm-ledger-narwhal-batch-header", + "snarkvm-ledger-narwhal-data", + "snarkvm-ledger-narwhal-subdag", + "snarkvm-ledger-narwhal-transmission", + "snarkvm-ledger-narwhal-transmission-id", +] + +[[package]] +name = "snarkvm-ledger-narwhal-batch-certificate" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "indexmap", + "rayon", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-narwhal-batch-header", + "snarkvm-ledger-narwhal-transmission-id", +] + +[[package]] +name = "snarkvm-ledger-narwhal-batch-header" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "indexmap", + "rayon", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-narwhal-transmission-id", +] + +[[package]] +name = "snarkvm-ledger-narwhal-data" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "bytes", + "serde_json", + "snarkvm-console", + "tokio", +] + +[[package]] +name = "snarkvm-ledger-narwhal-subdag" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "indexmap", + "rayon", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-committee", + "snarkvm-ledger-narwhal-batch-certificate", + "snarkvm-ledger-narwhal-batch-header", + "snarkvm-ledger-narwhal-transmission-id", +] + +[[package]] +name = "snarkvm-ledger-narwhal-transmission" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "bytes", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-block", + "snarkvm-ledger-narwhal-data", + "snarkvm-ledger-puzzle", +] + +[[package]] +name = "snarkvm-ledger-narwhal-transmission-id" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "snarkvm-console", + "snarkvm-ledger-puzzle", +] + +[[package]] +name = "snarkvm-ledger-puzzle" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "bincode", + "indexmap", + "lru", + "parking_lot", + "rand", + "rand_chacha", + "rayon", + "serde_json", + "snarkvm-algorithms", + "snarkvm-console", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-ledger-puzzle-epoch" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "colored", + "indexmap", + "lru", + "parking_lot", + "rand", + "rand_chacha", + "rayon", + "snarkvm-circuit", + "snarkvm-console", + "snarkvm-ledger-puzzle", + "snarkvm-synthesizer-process", + "snarkvm-synthesizer-program", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-ledger-query" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "async-trait", + "reqwest", + "serde", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-block", + "snarkvm-ledger-store", + "snarkvm-synthesizer-program", + "ureq", +] + +[[package]] +name = "snarkvm-ledger-store" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std-storage", + "anyhow", + "bincode", + "indexmap", + "parking_lot", + "rayon", + "serde", + "serde_json", + "snarkvm-console", + "snarkvm-ledger-authority", + "snarkvm-ledger-block", + "snarkvm-ledger-committee", + "snarkvm-ledger-narwhal-batch-certificate", + "snarkvm-ledger-puzzle", + "snarkvm-synthesizer-program", + "snarkvm-synthesizer-snark", + "snarkvm-utilities", + "tracing", +] + +[[package]] +name = "snarkvm-parameters" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "cfg-if", + "colored", + "encoding", + "hex", + "js-sys", + "lazy_static", + "parking_lot", + "paste", + "rand", + "serde_json", + "sha2", + "snarkvm-curves", + "snarkvm-utilities", + "thiserror 2.0.18", + "ureq", + "web-sys", +] + +[[package]] +name = "snarkvm-synthesizer" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "indexmap", + "itertools", + "lru", + "parking_lot", + "rand", + "rayon", + "serde_json", + "snarkvm-algorithms", + "snarkvm-circuit", + "snarkvm-console", + "snarkvm-ledger-authority", + "snarkvm-ledger-block", + "snarkvm-ledger-committee", + "snarkvm-ledger-narwhal-data", + "snarkvm-ledger-puzzle", + "snarkvm-ledger-puzzle-epoch", + "snarkvm-ledger-query", + "snarkvm-ledger-store", + "snarkvm-synthesizer-error", + "snarkvm-synthesizer-process", + "snarkvm-synthesizer-program", + "snarkvm-synthesizer-snark", + "snarkvm-utilities", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "snarkvm-synthesizer-error" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "anyhow", + "snarkvm-circuit-environment", + "snarkvm-console-network", + "snarkvm-console-program", + "thiserror 2.0.18", +] + +[[package]] +name = "snarkvm-synthesizer-process" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "colored", + "indexmap", + "itertools", + "parking_lot", + "rand", + "rand_chacha", + "rayon", + "serde_json", + "snarkvm-algorithms", + "snarkvm-circuit", + "snarkvm-console", + "snarkvm-ledger-block", + "snarkvm-ledger-query", + "snarkvm-ledger-store", + "snarkvm-synthesizer-error", + "snarkvm-synthesizer-program", + "snarkvm-synthesizer-snark", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-synthesizer-program" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "enum-iterator", + "indexmap", + "paste", + "rand", + "rand_chacha", + "rayon", + "serde_json", + "snarkvm-algorithms", + "snarkvm-circuit", + "snarkvm-console", + "snarkvm-synthesizer-error", + "snarkvm-synthesizer-snark", + "snarkvm-utilities", + "tiny-keccak", +] + +[[package]] +name = "snarkvm-synthesizer-snark" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "bincode", + "serde_json", + "snarkvm-algorithms", + "snarkvm-circuit", + "snarkvm-console", + "snarkvm-utilities", +] + +[[package]] +name = "snarkvm-utilities" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "aleo-std", + "anyhow", + "bincode", + "colored", + "num-bigint", + "num_cpus", + "rand", + "rand_xorshift", + "rayon", + "serde", + "serde_json", + "smol_str", + "snarkvm-utilities-derives", + "thiserror 2.0.18", + "tracing", + "zeroize", +] + +[[package]] +name = "snarkvm-utilities-derives" +version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM?tag=v4.8.1#b7f0859592c75dd251430377240c7697a37ab899" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "cookie_store", + "der", + "flate2", + "log", + "native-tls", + "percent-encoding", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf8-zero", + "webpki-root-certs", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote 1.0.46", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/sdk-abi/Cargo.toml b/sdk-abi/Cargo.toml new file mode 100644 index 0000000..eff95ab --- /dev/null +++ b/sdk-abi/Cargo.toml @@ -0,0 +1,22 @@ +# Copyright (C) 2024 Provable Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +[workspace] # Prevent parent workspace capture + +[package] +name = "aleo-abi" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-or-later" +description = "Python bindings for ABI generation from Aleo bytecode (via Leo's leo-abi crate)" + +[lib] +name = "_aleo_abi" +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +pyo3 = { version = "0.20.0", features = ["extension-module", "abi3-py37", "anyhow"] } +serde_json = "1" +leo-abi = { git = "https://github.com/ProvableHQ/leo", rev = "ba2c01722a48f84b4d90b93aeaf8305b7c03dbce", package = "leo-abi", features = ["aleo-bytecode"] } +leo-ast = { git = "https://github.com/ProvableHQ/leo", rev = "ba2c01722a48f84b4d90b93aeaf8305b7c03dbce", package = "leo-ast" } diff --git a/sdk-abi/pyproject.toml b/sdk-abi/pyproject.toml new file mode 100644 index 0000000..26e4078 --- /dev/null +++ b/sdk-abi/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "aleo-abi" +version = "0.1.0" +description = "Python bindings for ABI generation from Aleo bytecode" +license = {text = "GPL-3.0-or-later"} +requires-python = ">=3.7" +dependencies = [] + +[tool.maturin] +python-source = "python" +module-name = "aleo_abi._aleo_abi" diff --git a/sdk-abi/python/aleo_abi/__init__.py b/sdk-abi/python/aleo_abi/__init__.py new file mode 100644 index 0000000..09397ca --- /dev/null +++ b/sdk-abi/python/aleo_abi/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2024 Provable Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +"""ABI generation and compatibility checking for Aleo programs.""" + +from ._aleo_abi import generate_abi, check_compatibility + +__all__ = ["generate_abi", "check_compatibility"] diff --git a/sdk-abi/python/aleo_abi/_aleo_abi.pyi b/sdk-abi/python/aleo_abi/_aleo_abi.pyi new file mode 100644 index 0000000..12bbf12 --- /dev/null +++ b/sdk-abi/python/aleo_abi/_aleo_abi.pyi @@ -0,0 +1,2 @@ +def generate_abi(program_name: str, bytecode: str, network: str) -> str: ... +def check_compatibility(candidate_abi_json: str, standard_abi_json: str) -> list[str]: ... diff --git a/sdk-abi/python/aleo_abi/py.typed b/sdk-abi/python/aleo_abi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/sdk-abi/python/tests/test_abi.py b/sdk-abi/python/tests/test_abi.py new file mode 100644 index 0000000..47b990d --- /dev/null +++ b/sdk-abi/python/tests/test_abi.py @@ -0,0 +1,238 @@ +# Copyright (C) 2024 Provable Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +"""Tests for the aleo-abi package.""" + +import json + +import pytest + +import aleo_abi + + +# --------------------------------------------------------------------------- +# Fixtures vendored from leo/tests/tests/cli/test_abi_from_aleo/contents/ +# --------------------------------------------------------------------------- + +SIMPLE_ALEO = """\ +program simple.aleo; + +struct Point: + x as i32; + y as i32; + +record Token: + owner as address.private; + amount as u64.public; + +mapping balances: + key as address.public; + value as u64.public; + +function add_numbers: + input r0 as u32.private; + input r1 as u32.private; + add r0 r1 into r2; + output r2 as u32.private; + +function get_point: + input r0 as Point.private; + output r0 as Point.private; + +function get_token: + input r0 as Token.record; + output r0 as Token.record;""" + +# Vendored from leo/tests/expectations/cli/test_abi_from_aleo/contents/simple_out/simple.aleo.abi.json +SIMPLE_EXPECTED = { + "program": "simple.aleo", + "structs": [ + { + "path": ["Point"], + "fields": [ + {"name": "x", "ty": {"Primitive": {"Int": "I32"}}}, + {"name": "y", "ty": {"Primitive": {"Int": "I32"}}}, + ], + } + ], + "records": [ + { + "path": ["Token"], + "fields": [ + { + "name": "owner", + "ty": {"Primitive": "Address"}, + "mode": "Public", + }, + { + "name": "amount", + "ty": {"Primitive": {"UInt": "U64"}}, + "mode": "Private", + }, + ], + } + ], + "mappings": [ + { + "name": "balances", + "key": {"Primitive": "Address"}, + "value": {"Primitive": {"UInt": "U64"}}, + } + ], + "storage_variables": [], + "functions": [ + { + "name": "add_numbers", + "inputs": [ + { + "Plaintext": { + "ty": {"Primitive": {"UInt": "U32"}}, + "mode": "Private", + } + }, + { + "Plaintext": { + "ty": {"Primitive": {"UInt": "U32"}}, + "mode": "Private", + } + }, + ], + "outputs": [ + { + "Plaintext": { + "ty": {"Primitive": {"UInt": "U32"}}, + "mode": "Private", + } + } + ], + }, + { + "name": "get_point", + "inputs": [ + { + "Plaintext": { + "ty": { + "Struct": { + "path": ["Point"], + "program": "simple.aleo", + } + }, + "mode": "Private", + } + } + ], + "outputs": [ + { + "Plaintext": { + "ty": { + "Struct": { + "path": ["Point"], + "program": "simple.aleo", + } + }, + "mode": "Private", + } + } + ], + }, + { + "name": "get_token", + "inputs": [ + { + "Record": { + "path": ["Token"], + "program": "simple.aleo", + } + } + ], + "outputs": [ + { + "Record": { + "path": ["Token"], + "program": "simple.aleo", + } + } + ], + }, + ], +} + + +# --------------------------------------------------------------------------- +# generate_abi tests +# --------------------------------------------------------------------------- + + +def test_generate_abi_simple(): + """generate_abi on simple.aleo produces the expected ABI.""" + result_json = aleo_abi.generate_abi("simple.aleo", SIMPLE_ALEO, "testnet") + result = json.loads(result_json) + assert result == SIMPLE_EXPECTED + + +def test_generate_abi_returns_string(): + """generate_abi returns a JSON string.""" + result = aleo_abi.generate_abi("simple.aleo", SIMPLE_ALEO, "testnet") + assert isinstance(result, str) + parsed = json.loads(result) + assert parsed["program"] == "simple.aleo" + + +def test_generate_abi_mainnet(): + """generate_abi works with 'mainnet' network.""" + result_json = aleo_abi.generate_abi("simple.aleo", SIMPLE_ALEO, "mainnet") + result = json.loads(result_json) + assert result["program"] == "simple.aleo" + + +def test_generate_abi_unknown_network(): + """generate_abi raises on unknown network.""" + with pytest.raises(Exception, match="unknown network"): + aleo_abi.generate_abi("simple.aleo", SIMPLE_ALEO, "banana") + + +# --------------------------------------------------------------------------- +# check_compatibility tests +# --------------------------------------------------------------------------- + + +def test_check_compatibility_identical(): + """Identical ABIs produce no violations.""" + abi_json = json.dumps(SIMPLE_EXPECTED) + violations = aleo_abi.check_compatibility(abi_json, abi_json) + assert violations == [] + + +def test_check_compatibility_missing_function(): + """Candidate missing a function that standard requires → violation.""" + # Standard has all functions; candidate is missing get_token + candidate = dict(SIMPLE_EXPECTED) + candidate["functions"] = [ + f for f in SIMPLE_EXPECTED["functions"] if f["name"] != "get_token" + ] + violations = aleo_abi.check_compatibility( + json.dumps(candidate), json.dumps(SIMPLE_EXPECTED) + ) + assert len(violations) > 0 + assert any("get_token" in v for v in violations) + + +def test_check_compatibility_different_signature(): + """Candidate function with different signature → violation.""" + # Mutate add_numbers to have a public output instead of private + import copy + + candidate = copy.deepcopy(SIMPLE_EXPECTED) + for fn in candidate["functions"]: + if fn["name"] == "add_numbers": + fn["outputs"][0]["Plaintext"]["mode"] = "Public" + violations = aleo_abi.check_compatibility( + json.dumps(candidate), json.dumps(SIMPLE_EXPECTED) + ) + assert len(violations) > 0 + + +def test_check_compatibility_returns_list(): + """check_compatibility always returns a list.""" + abi_json = json.dumps(SIMPLE_EXPECTED) + result = aleo_abi.check_compatibility(abi_json, abi_json) + assert isinstance(result, list) diff --git a/sdk-abi/src/lib.rs b/sdk-abi/src/lib.rs new file mode 100644 index 0000000..6305912 --- /dev/null +++ b/sdk-abi/src/lib.rs @@ -0,0 +1,68 @@ +// Copyright (C) 2024 Provable Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +//! Python bindings for leo-abi: ABI generation and compatibility checking for Aleo/Leo programs. + +// pyo3 0.20 generates unsafe fn calls inside its macros; the warning is benign +// with edition 2024 where pyo3 0.20 predates the edition-2024 unsafe_op_in_unsafe_fn lint. +#![allow(unsafe_op_in_unsafe_fn)] + +use pyo3::prelude::*; + +/// Parse the network name string into leo_ast::NetworkName. +fn parse_network(network: &str) -> anyhow::Result<leo_ast::NetworkName> { + match network { + "mainnet" => Ok(leo_ast::NetworkName::MainnetV0), + "testnet" => Ok(leo_ast::NetworkName::TestnetV0), + "canary" => Ok(leo_ast::NetworkName::CanaryV0), + other => anyhow::bail!( + "unknown network {:?}; expected \"mainnet\", \"testnet\", or \"canary\"", + other + ), + } +} + +/// Generate an ABI JSON string from Aleo bytecode. +/// +/// Args: +/// program_name: The program name (e.g. "token.aleo"). +/// bytecode: The Aleo bytecode string. +/// network: One of "mainnet", "testnet", or "canary". +/// +/// Returns: +/// A pretty-printed JSON string representing the program ABI. +#[pyfunction] +fn generate_abi(program_name: &str, bytecode: &str, network: &str) -> anyhow::Result<String> { + let net = parse_network(network)?; + let abi = leo_abi::aleo::generate_from_bytecode(program_name, bytecode, net) + .map_err(|e| anyhow::anyhow!("{}", e))?; + Ok(serde_json::to_string_pretty(&abi)?) +} + +/// Check whether a candidate ABI JSON is compatible with a standard ABI JSON. +/// +/// Args: +/// candidate_abi_json: JSON string of the candidate program's ABI. +/// standard_abi_json: JSON string of the standard/interface ABI. +/// +/// Returns: +/// A list of violation strings. Empty list means compatible. +#[pyfunction] +fn check_compatibility( + candidate_abi_json: &str, + standard_abi_json: &str, +) -> anyhow::Result<Vec<String>> { + let candidate: leo_abi::Program = serde_json::from_str(candidate_abi_json) + .map_err(|e| anyhow::anyhow!("failed to parse candidate ABI JSON: {}", e))?; + let standard: leo_abi::Program = serde_json::from_str(standard_abi_json) + .map_err(|e| anyhow::anyhow!("failed to parse standard ABI JSON: {}", e))?; + Ok(leo_abi::compatibility::check_compatibility( + &candidate, &standard, + )) +} + +#[pymodule] +fn _aleo_abi(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(generate_abi, m)?)?; + m.add_function(wrap_pyfunction!(check_compatibility, m)?)?; + Ok(()) +} diff --git a/sdk/python/aleo/__init__.py b/sdk/python/aleo/__init__.py index 3ba0780..6929d3b 100644 --- a/sdk/python/aleo/__init__.py +++ b/sdk/python/aleo/__init__.py @@ -15,3 +15,13 @@ RecordNotFoundError as RecordNotFoundError, UUIDError as UUIDError, ) + + +def __getattr__(name: str) -> object: + if name == "abi": + import importlib + mod = importlib.import_module(".abi", __name__) + # Cache it so subsequent accesses don't call __getattr__ again + globals()["abi"] = mod + return mod + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/sdk/python/aleo/abi.py b/sdk/python/aleo/abi.py new file mode 100644 index 0000000..51202f8 --- /dev/null +++ b/sdk/python/aleo/abi.py @@ -0,0 +1,91 @@ +# Copyright (C) 2024 Provable Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +"""Integration hook: ABI generation via the aleo-abi package.""" +from __future__ import annotations + +import json +import re +from typing import Union + + +def generate_abi(program: object, network: str = "mainnet") -> dict: + """Generate an ABI dict for an Aleo program. + + Args: + program: Either an aleo Program object (with .source and .id attributes) + or a raw bytecode string. + network: Network name: "mainnet", "testnet", or "canary". + + Returns: + A dict containing the ABI for the program. + + Raises: + ImportError: If the aleo-abi package is not installed. + ValueError: If the program name cannot be determined. + """ + try: + import aleo_abi as _aleo_abi + except ImportError: + raise ImportError( + "The aleo-abi package is required for ABI generation. " + "Install it with: pip install aleo-abi" + ) + + # Duck-type: if it has .source and .id, treat as Program object + if hasattr(program, "source") and hasattr(program, "id"): + bytecode: str = program.source + name: str = str(program.id) + elif isinstance(program, str): + bytecode = program + # Parse name from `program X.aleo;` line + m = re.search(r"program\s+(\S+\.aleo)\s*;", bytecode) + if m is None: + raise ValueError( + "Could not determine program name from bytecode. " + "Expected a line like: program foo.aleo;" + ) + name = m.group(1) + else: + raise TypeError( + f"Expected a Program object or str, got {type(program).__name__}" + ) + + json_str = _aleo_abi.generate_abi(name, bytecode, network) + return json.loads(json_str) + + +def check_compatibility( + candidate: Union[dict, str], + standard: Union[dict, str], +) -> list: + """Check whether a candidate ABI is compatible with a standard ABI. + + Args: + candidate: The candidate program ABI as a dict or JSON string. + standard: The standard/interface ABI as a dict or JSON string. + + Returns: + A list of violation strings. Empty list means compatible. + + Raises: + ImportError: If the aleo-abi package is not installed. + """ + try: + import aleo_abi as _aleo_abi + except ImportError: + raise ImportError( + "The aleo-abi package is required for compatibility checking. " + "Install it with: pip install aleo-abi" + ) + + if isinstance(candidate, dict): + candidate_json = json.dumps(candidate) + else: + candidate_json = candidate + + if isinstance(standard, dict): + standard_json = json.dumps(standard) + else: + standard_json = standard + + return _aleo_abi.check_compatibility(candidate_json, standard_json) diff --git a/sdk/python/tests/test_abi_hook.py b/sdk/python/tests/test_abi_hook.py new file mode 100644 index 0000000..ca59762 --- /dev/null +++ b/sdk/python/tests/test_abi_hook.py @@ -0,0 +1,73 @@ +"""Tests for the aleo.abi integration hook.""" +import json +import sys + +import pytest + + +SIMPLE_ALEO = """program simple.aleo; + +struct Point: + x as i32; + y as i32; + +record Token: + owner as address.private; + amount as u64.public; + +mapping balances: + key as address.public; + value as u64.public; + +function add_numbers: + input r0 as u32.private; + input r1 as u32.private; + add r0 r1 into r2; + output r2 as u32.private; + +function get_point: + input r0 as Point.private; + output r0 as Point.private; + +function get_token: + input r0 as Token.record; + output r0 as Token.record;""" + + +def test_generate_abi_credits(): + """generate_abi(Program.credits()) returns dict with program=='credits.aleo'.""" + from aleo import abi + from aleo.mainnet import Program + credits = Program.credits() + result = abi.generate_abi(credits) + assert isinstance(result, dict) + assert result["program"] == "credits.aleo" + assert len(result.get("functions", [])) > 0 + + +def test_generate_abi_string_input(): + """generate_abi works with a raw bytecode string.""" + from aleo import abi + result = abi.generate_abi(SIMPLE_ALEO, network="testnet") + assert isinstance(result, dict) + assert result["program"] == "simple.aleo" + assert len(result.get("functions", [])) > 0 + + +def test_generate_abi_lazy_import_error(): + """Helpful ImportError when aleo_abi is not installed.""" + # Hide the aleo_abi package from sys.modules + saved = sys.modules.pop("aleo_abi", None) + # Also block future imports + sys.modules["aleo_abi"] = None # type: ignore[assignment] + try: + from aleo import abi + import importlib + importlib.reload(abi) + with pytest.raises(ImportError, match="pip install aleo-abi"): + abi.generate_abi(SIMPLE_ALEO) + finally: + if saved is None: + sys.modules.pop("aleo_abi", None) + else: + sys.modules["aleo_abi"] = saved From 97547c934c93cfed322f3478c85ccc21097675c1 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 15:34:11 -0400 Subject: [PATCH 66/72] ci: test-abi lane builds and tests aleo-abi; guard hook tests; isolation canary - new test-abi job: builds both wheels, runs the aleo-abi KAT suite and the main-package hook tests for real, and asserts sdk/Cargo.lock never references leo's dev_skip_checks feature (build-graph isolation canary) - hook tests skip gracefully where aleo-abi isn't installed (3-OS matrix) - abi wheel jobs gated on the tests workflow like the main wheels Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .github/workflows/sdk-wheels.yml | 4 +-- .github/workflows/sdk.yml | 41 +++++++++++++++++++++++++++++++ sdk/python/tests/test_abi_hook.py | 9 +++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sdk-wheels.yml b/.github/workflows/sdk-wheels.yml index fa53c8c..8c598ef 100644 --- a/.github/workflows/sdk-wheels.yml +++ b/.github/workflows/sdk-wheels.yml @@ -99,7 +99,7 @@ jobs: build-abi: name: abi-build (${{ matrix.platform.name }}) - needs: [] + needs: [tests] runs-on: ${{ matrix.platform.runner }} strategy: fail-fast: false @@ -168,7 +168,7 @@ jobs: path: sdk/dist sdist-abi: - needs: [] + needs: [tests] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 3f412bb..35b82a7 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -101,3 +101,44 @@ jobs: # would prove twice, so they run in a single worker on purpose. - name: pytest (proving suite) run: python -m pytest python/tests -v -m slow + + test-abi: + name: Test (aleo-abi package) + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + sdk + sdk-abi + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + # The canary for build-graph isolation: leo's snarkvm feature flags + # (dev_skip_checks etc.) must never leak into the main SDK's lockfile. + - name: Assert main-crate isolation from leo features + run: | + count=$(grep -c "dev_skip_checks" sdk/Cargo.lock || true) + if [ "$count" != "0" ]; then + echo "::error::sdk/Cargo.lock references dev_skip_checks — leo build graph leaked into the main crate" + exit 1 + fi + - name: Build both wheels + run: | + pip install maturin pytest requests + maturin build --release --features mainnet --manifest-path sdk/Cargo.toml --out dist + maturin build --release --manifest-path sdk-abi/Cargo.toml --out dist + - name: Install wheels + run: | + pip install --no-index --find-links dist aleo aleo-abi + - name: pytest (aleo-abi package suite) + run: python -m pytest sdk-abi/python/tests -v + - name: pytest (main-package abi hook) + run: | + cd sdk + python -m pytest python/tests/test_abi_hook.py -v diff --git a/sdk/python/tests/test_abi_hook.py b/sdk/python/tests/test_abi_hook.py index ca59762..2c13122 100644 --- a/sdk/python/tests/test_abi_hook.py +++ b/sdk/python/tests/test_abi_hook.py @@ -1,4 +1,5 @@ """Tests for the aleo.abi integration hook.""" +import importlib.util import json import sys @@ -34,6 +35,13 @@ output r0 as Token.record;""" +_HAS_ALEO_ABI = importlib.util.find_spec("aleo_abi") is not None +requires_aleo_abi = pytest.mark.skipif( + not _HAS_ALEO_ABI, reason="aleo-abi not installed (pip install aleo-abi)" +) + + +@requires_aleo_abi def test_generate_abi_credits(): """generate_abi(Program.credits()) returns dict with program=='credits.aleo'.""" from aleo import abi @@ -45,6 +53,7 @@ def test_generate_abi_credits(): assert len(result.get("functions", [])) > 0 +@requires_aleo_abi def test_generate_abi_string_input(): """generate_abi works with a raw bytecode string.""" from aleo import abi From 603d486d3aa7486fb6eea82c2348b11500176bd9 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 16:05:27 -0400 Subject: [PATCH 67/72] ci: wheel installs must resolve runtime deps; pyright env gets package imports The aleo package now genuinely depends on requests (PEP 621 fix), so --no-index wheel installs broke every lane. Allow dependency resolution, add the new client test deps to test lanes, and give the pyright action an environment that can resolve requests/httpx/nacl imports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .github/workflows/sdk-wheels.yml | 4 ++-- .github/workflows/sdk.yml | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sdk-wheels.yml b/.github/workflows/sdk-wheels.yml index 8c598ef..dbd89ca 100644 --- a/.github/workflows/sdk-wheels.yml +++ b/.github/workflows/sdk-wheels.yml @@ -83,7 +83,7 @@ jobs: - name: Smoke test wheel shell: bash run: | - pip install --no-index --find-links sdk/dist aleo + pip install --find-links sdk/dist aleo python -c " from aleo.mainnet import PrivateKey pk = PrivateKey.random() @@ -142,7 +142,7 @@ jobs: - name: Smoke test wheel shell: bash run: | - pip install --no-index --find-links sdk-abi/dist aleo-abi + pip install --find-links sdk-abi/dist aleo-abi python -c "import aleo_abi; print(aleo_abi.generate_abi.__name__)" - name: Upload wheel uses: actions/upload-artifact@v4 diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 35b82a7..f7aea54 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -28,6 +28,12 @@ jobs: # Guard the testnet cfg seam — it only exists at compile time. - name: check (testnet) run: cargo check --no-default-features --features testnet + # pyright needs the package's third-party imports resolvable + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install import-resolution deps for pyright + run: pip install requests httpx pynacl - uses: jordemort/action-pyright@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -60,8 +66,8 @@ jobs: maturin build --release --features mainnet --out dist - name: Install wheel + test deps run: | - pip install --no-index --find-links dist aleo - pip install pytest pytest-xdist + pip install --find-links dist aleo + pip install pytest pytest-xdist httpx pynacl responses pytest-asyncio # Fast, offline tests; -n auto parallelizes across cores. - name: pytest (fast suite) run: python -m pytest python/tests -v -n auto -m "not slow" @@ -95,8 +101,8 @@ jobs: maturin build --release --features mainnet --out dist - name: Install wheel + test deps run: | - pip install --no-index --find-links dist aleo - pip install pytest + pip install --find-links dist aleo + pip install pytest httpx pynacl responses pytest-asyncio # The two slow tests share one module-scoped proven execution; xdist # would prove twice, so they run in a single worker on purpose. - name: pytest (proving suite) @@ -135,7 +141,7 @@ jobs: maturin build --release --manifest-path sdk-abi/Cargo.toml --out dist - name: Install wheels run: | - pip install --no-index --find-links dist aleo aleo-abi + pip install --find-links dist aleo aleo-abi - name: pytest (aleo-abi package suite) run: python -m pytest sdk-abi/python/tests -v - name: pytest (main-package abi hook) From eae64d8c0d6a6f9ffeef8f0af30412ba6922492f Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 16:09:16 -0400 Subject: [PATCH 68/72] fix: strict-pyright compliance for aleo.abi hook (generics, guarded optional import) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/abi.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sdk/python/aleo/abi.py b/sdk/python/aleo/abi.py index 51202f8..db5cb7f 100644 --- a/sdk/python/aleo/abi.py +++ b/sdk/python/aleo/abi.py @@ -5,10 +5,10 @@ import json import re -from typing import Union +from typing import Any, Union -def generate_abi(program: object, network: str = "mainnet") -> dict: +def generate_abi(program: object, network: str = "mainnet") -> dict[str, Any]: """Generate an ABI dict for an Aleo program. Args: @@ -24,7 +24,7 @@ def generate_abi(program: object, network: str = "mainnet") -> dict: ValueError: If the program name cannot be determined. """ try: - import aleo_abi as _aleo_abi + import aleo_abi as _aleo_abi # pyright: ignore[reportMissingImports] except ImportError: raise ImportError( "The aleo-abi package is required for ABI generation. " @@ -33,8 +33,8 @@ def generate_abi(program: object, network: str = "mainnet") -> dict: # Duck-type: if it has .source and .id, treat as Program object if hasattr(program, "source") and hasattr(program, "id"): - bytecode: str = program.source - name: str = str(program.id) + bytecode: str = getattr(program, "source") + name: str = str(getattr(program, "id")) elif isinstance(program, str): bytecode = program # Parse name from `program X.aleo;` line @@ -55,9 +55,9 @@ def generate_abi(program: object, network: str = "mainnet") -> dict: def check_compatibility( - candidate: Union[dict, str], - standard: Union[dict, str], -) -> list: + candidate: Union[dict[str, Any], str], + standard: Union[dict[str, Any], str], +) -> list[str]: """Check whether a candidate ABI is compatible with a standard ABI. Args: @@ -71,7 +71,7 @@ def check_compatibility( ImportError: If the aleo-abi package is not installed. """ try: - import aleo_abi as _aleo_abi + import aleo_abi as _aleo_abi # pyright: ignore[reportMissingImports] except ImportError: raise ImportError( "The aleo-abi package is required for compatibility checking. " @@ -88,4 +88,5 @@ def check_compatibility( else: standard_json = standard - return _aleo_abi.check_compatibility(candidate_json, standard_json) + result: list[str] = _aleo_abi.check_compatibility(candidate_json, standard_json) + return result From 85a53ef8ce49d5663d7315a9c6cd9df83f7e3383 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 16:12:38 -0400 Subject: [PATCH 69/72] ci: scope pyright unknown-type rules off at untyped/optional third-party boundaries pynacl/httpx are optional deps and aleo_abi is a separate package; strict pyright's reportUnknownMemberType cascades from those boundary calls. Relax only those three rules in the four glue modules; the ~40 core binding modules stay fully strict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/abi.py | 1 + sdk/python/aleo/async_network_client.py | 1 + sdk/python/aleo/async_record_scanner.py | 1 + sdk/python/aleo/security.py | 1 + 4 files changed, 4 insertions(+) diff --git a/sdk/python/aleo/abi.py b/sdk/python/aleo/abi.py index db5cb7f..7722131 100644 --- a/sdk/python/aleo/abi.py +++ b/sdk/python/aleo/abi.py @@ -1,5 +1,6 @@ # Copyright (C) 2024 Provable Inc. # SPDX-License-Identifier: GPL-3.0-or-later +# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false """Integration hook: ABI generation via the aleo-abi package.""" from __future__ import annotations diff --git a/sdk/python/aleo/async_network_client.py b/sdk/python/aleo/async_network_client.py index 1fbd119..64c2466 100644 --- a/sdk/python/aleo/async_network_client.py +++ b/sdk/python/aleo/async_network_client.py @@ -1,3 +1,4 @@ +# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false """Asynchronous Aleo network client (httpx-based). Transport contract (async) diff --git a/sdk/python/aleo/async_record_scanner.py b/sdk/python/aleo/async_record_scanner.py index 6043715..f9eea6b 100644 --- a/sdk/python/aleo/async_record_scanner.py +++ b/sdk/python/aleo/async_record_scanner.py @@ -1,3 +1,4 @@ +# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false """Asynchronous RecordScanner (httpx-based) for delegated record scanning.""" from __future__ import annotations diff --git a/sdk/python/aleo/security.py b/sdk/python/aleo/security.py index 6d1d1ea..a5c981d 100644 --- a/sdk/python/aleo/security.py +++ b/sdk/python/aleo/security.py @@ -1,3 +1,4 @@ +# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false """PyNaCl sealed-box helpers for DPS encryption.""" from __future__ import annotations From 583d497dd0f40a04b7146f992afc6a0021ffb2b4 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 16:22:46 -0400 Subject: [PATCH 70/72] ci: give sdk-abi its own pytest.ini; run test-abi suites from package dirs The legacy root setup.cfg injects --numprocesses/--timeout addopts (needs xdist/timeout plugins) into any pytest run from repo root. Running each suite from inside its package dir makes pytest use that package's pytest.ini instead, so the test-abi lane needs no extra plugins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- .github/workflows/sdk.yml | 7 ++++++- sdk-abi/pytest.ini | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 sdk-abi/pytest.ini diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index f7aea54..d5e81b9 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -142,8 +142,13 @@ jobs: - name: Install wheels run: | pip install --find-links dist aleo aleo-abi + # Run each suite from inside its package dir so pytest uses that + # package's own pytest.ini (sdk-abi/pytest.ini, sdk/pytest.ini) rather + # than walking up to the stale legacy root setup.cfg. - name: pytest (aleo-abi package suite) - run: python -m pytest sdk-abi/python/tests -v + run: | + cd sdk-abi + python -m pytest python/tests -v - name: pytest (main-package abi hook) run: | cd sdk diff --git a/sdk-abi/pytest.ini b/sdk-abi/pytest.ini new file mode 100644 index 0000000..ae50458 --- /dev/null +++ b/sdk-abi/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = python/tests +python_files = test_*.py From f6f4a2e94aa425d41cd6cf1f4dc0f4cd01ac1de0 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 19:58:54 -0400 Subject: [PATCH 71/72] fix: lazy per-network ProvingRequest import in submit_proving_request_safe Passing a ProvingRequest object no longer triggers a mainnet import (broke testnet-only/bundled installs); the string-parse path imports the client's own network module. Found by the live delegated-proving spike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/network_client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sdk/python/aleo/network_client.py b/sdk/python/aleo/network_client.py index b7d6bd3..9e45cbc 100644 --- a/sdk/python/aleo/network_client.py +++ b/sdk/python/aleo/network_client.py @@ -508,13 +508,14 @@ def submit_proving_request_safe( if resolved_jwt and resolved_jwt.get("jwt"): hdrs["Authorization"] = resolved_jwt["jwt"] - # Parse the proving request to determine routing - try: - from .mainnet import ProvingRequest # type: ignore[attr-defined] - except ImportError: - raise ImportError("aleo mainnet module not available") from None - + # Determine routing. A ProvingRequest object is used as-is (no import); + # only a serialized string needs parsing, via THIS client's network + # module (not hardcoded mainnet). if isinstance(proving_request, str): + if self._network == "testnet": + from .testnet import ProvingRequest # type: ignore[attr-defined] + else: + from .mainnet import ProvingRequest # type: ignore[attr-defined] pr_obj = ProvingRequest.from_string(proving_request) else: pr_obj = proving_request From 422f242640de8d8b3e5f4174f6ebb10720295de0 Mon Sep 17 00:00:00 2001 From: Michael Turner <mike@provable.com> Date: Thu, 9 Jul 2026 20:08:47 -0400 Subject: [PATCH 72/72] fix: make ProvingRequest import lazy (object path imports nothing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects the prior attempt: reverts to mainnet-only lazy import (no premature .testnet reference — that module isn't shipped yet), matching the previously type-clean code. Object callers (delegate()) trigger no import; only the string-parse path imports. Becomes network-aware when testnet ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- sdk/python/aleo/network_client.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sdk/python/aleo/network_client.py b/sdk/python/aleo/network_client.py index 9e45cbc..7a49b0b 100644 --- a/sdk/python/aleo/network_client.py +++ b/sdk/python/aleo/network_client.py @@ -508,14 +508,11 @@ def submit_proving_request_safe( if resolved_jwt and resolved_jwt.get("jwt"): hdrs["Authorization"] = resolved_jwt["jwt"] - # Determine routing. A ProvingRequest object is used as-is (no import); - # only a serialized string needs parsing, via THIS client's network - # module (not hardcoded mainnet). + # Determine routing. A ProvingRequest object is used as-is (no import — + # this is what delegate()/callers pass). Only a serialized string needs + # parsing; import lazily so an object never forces the module load. if isinstance(proving_request, str): - if self._network == "testnet": - from .testnet import ProvingRequest # type: ignore[attr-defined] - else: - from .mainnet import ProvingRequest # type: ignore[attr-defined] + from .mainnet import ProvingRequest # type: ignore[attr-defined] pr_obj = ProvingRequest.from_string(proving_request) else: pr_obj = proving_request