From cf9177a0954fa4d9796fc8086b87a03380602fff Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Thu, 21 May 2026 20:46:13 -0400 Subject: [PATCH] feat(wallet): add keycard support for public tx for auth-transfer (#451) * feat: add basic commands for communicating with keycard * initialize changes * reorganization * add script file for easier wallet access * update commands * fixes * fixed load for non continuous run * Updates for signatures with keycard * fix BIP-340 signatures for fixed sized messages * fmt * refactor and add pin support to program facades * fix unit test * fixes * Revert "fixes" This reverts commit 41f34f4ff4145b7abb60fd9bec168ae4b60f23b4. * fixes * fixes * Removed privacy keycard calls * Revert "Removed privacy keycard calls" This reverts commit d70ef505a1f40b87159099761f5fce5a31e3f17b. * Add domain separators * Removed privacy txs for keycard * CI fixes * CI fixes * addressed some comments * fix ci * ci fixes * fix integration test issue and updated keycard firmware * addressed more comments * fixed deny * remove keycard-py * fixed from earlier merge * add hash_message tests * add test * fix deny * CI fixes * fixed integration tests * Update public.rs * update artifacts * ci and comments * addressed comments * comment fixes * fixes from merging main * first round of comments * Revert "Merge branch 'main' into marvin/keycard-commands" This reverts commit 3fce53f663a3996938dddf77680854570063ca21, reversing changes made to e7b42a5177641455a8917bd2e29db20afd9690e5. * python comments * addressed comments * compile error fixed * fix artifacts * fix main merge error * adjust signer logic workflow * fmt * merge main and shift keycard tests * deny fix * artifacts fix * remove keycard scripts from root * tps fix * fmt --- Cargo.lock | 138 ++++++++++ Cargo.toml | 3 + .../malicious_injector.bin | Bin 403812 -> 403844 bytes .../malicious_launderer.bin | Bin 389560 -> 389588 bytes docs/LEZ testnet v0.1 tutorials/keycard.md | 237 ++++++++++++++++++ integration_tests/tests/tps.rs | 67 ++++- keycard_wallet/Cargo.toml | 15 ++ .../keycard_applets/LEE_keycard.cap | Bin 0 -> 102136 bytes keycard_wallet/keycard_applets/math.cap | Bin 0 -> 4874 bytes keycard_wallet/python/keycard_wallet.py | 164 ++++++++++++ keycard_wallet/src/lib.rs | 230 +++++++++++++++++ keycard_wallet/src/python_path.rs | 63 +++++ keycard_wallet/tests/keycard_tests.sh | 81 ++++++ keycard_wallet/wallet_with_keycard.sh | 12 + test_fixtures/src/lib.rs | 16 +- wallet-ffi/src/transfer.rs | 20 +- wallet/Cargo.toml | 4 + wallet/src/cli/keycard.rs | 136 ++++++++++ wallet/src/cli/mod.rs | 31 ++- .../src/cli/programs/native_token_transfer.rs | 37 ++- wallet/src/helperfunctions.rs | 27 ++ wallet/src/lib.rs | 3 + .../native_token_transfer/public.rs | 136 +++++----- wallet/src/signing.rs | 114 +++++++++ 24 files changed, 1454 insertions(+), 80 deletions(-) create mode 100644 docs/LEZ testnet v0.1 tutorials/keycard.md create mode 100644 keycard_wallet/Cargo.toml create mode 100644 keycard_wallet/keycard_applets/LEE_keycard.cap create mode 100644 keycard_wallet/keycard_applets/math.cap create mode 100644 keycard_wallet/python/keycard_wallet.py create mode 100644 keycard_wallet/src/lib.rs create mode 100644 keycard_wallet/src/python_path.rs create mode 100755 keycard_wallet/tests/keycard_tests.sh create mode 100755 keycard_wallet/wallet_with_keycard.sh create mode 100644 wallet/src/cli/keycard.rs create mode 100644 wallet/src/signing.rs diff --git a/Cargo.lock b/Cargo.lock index 7255dbee..2d3a8ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4018,6 +4018,15 @@ dependencies = [ "web-time", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "inout" version = "0.1.4" @@ -4486,6 +4495,17 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "keycard_wallet" +version = "0.1.0" +dependencies = [ + "log", + "nssa", + "pyo3", + "serde", + "serde_json", +] + [[package]] name = "lazy-regex" version = "3.6.0" @@ -6057,6 +6077,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mempool" version = "0.1.0" @@ -7337,6 +7366,69 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "pyo3" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.117", +] + [[package]] name = "quick-protobuf" version = "0.8.1" @@ -8151,6 +8243,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" +[[package]] +name = "rpassword" +version = "7.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + [[package]] name = "rpds" version = "1.2.0" @@ -8243,6 +8346,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "ruint" version = "1.17.2" @@ -9266,6 +9379,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.26.0" @@ -10145,6 +10264,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "unit-prefix" version = "0.5.2" @@ -10326,11 +10451,14 @@ dependencies = [ "indicatif", "itertools 0.14.0", "key_protocol", + "keycard_wallet", "log", "nssa", "nssa_core", "optfield", + "pyo3", "rand 0.8.5", + "rpassword", "sequencer_service_rpc", "serde", "serde_json", @@ -10341,6 +10469,7 @@ dependencies = [ "token_core", "tokio", "url", + "zeroize", ] [[package]] @@ -10756,6 +10885,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index d3b0921c..f4a981ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "examples/program_deployment/methods/guest", "testnet_initial_state", "indexer/ffi", + "keycard_wallet", "test_fixtures", "tools/cycle_bench", "tools/crypto_primitives_bench", @@ -77,6 +78,7 @@ faucet_core = { path = "programs/faucet/core" } vault_core = { path = "programs/vault/core" } test_program_methods = { path = "test_program_methods" } testnet_initial_state = { path = "testnet_initial_state" } +keycard_wallet = { path = "keycard_wallet" } test_fixtures = { path = "test_fixtures" } tokio = { version = "1.50", features = [ @@ -158,6 +160,7 @@ actix-web = { version = "4.13.0", default-features = false, features = [ ] } clap = { version = "4.5.42", features = ["derive", "env"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } +pyo3 = { version = "0.24", features = ["auto-initialize"] } # Profile for leptos WASM release builds [profile.wasm-release] diff --git a/artifacts/test_program_methods/malicious_injector.bin b/artifacts/test_program_methods/malicious_injector.bin index 40e57b81bc6ccf85550092f83b72d8875ea4a54e..55ac856b1177197fff3b124cdf3c240057e59f6a 100644 GIT binary patch delta 85203 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