diff --git a/Cargo.lock b/Cargo.lock index e81f6326..3a6c10c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2098,7 +2098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2582,7 +2582,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]] @@ -3561,7 +3561,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -3924,6 +3924,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" @@ -4392,6 +4401,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" @@ -5963,6 +5983,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" @@ -7199,6 +7228,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" @@ -7235,7 +7327,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -7272,7 +7364,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.52.0", ] @@ -8013,6 +8105,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" @@ -8105,6 +8208,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" @@ -8167,7 +8280,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9128,6 +9241,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" @@ -9138,7 +9257,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9997,6 +10116,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" @@ -10178,11 +10303,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", @@ -10193,6 +10321,7 @@ dependencies = [ "token_core", "tokio", "url", + "zeroize", ] [[package]] @@ -10469,7 +10598,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -10608,6 +10737,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/docs/LEZ testnet v0.1 tutorials/keycard.md b/docs/LEZ testnet v0.1 tutorials/keycard.md index 92224292..38feea4f 100644 --- a/docs/LEZ testnet v0.1 tutorials/keycard.md +++ b/docs/LEZ testnet v0.1 tutorials/keycard.md @@ -27,7 +27,7 @@ Installation: - **Important:** keycard can only connect with one application at a time; if Keycard-Desktop is using keycard then Wallet CLI cannot access the same keycard, and vice-versa. ## Wallet with Keycard -Keycard functionality is available to Wallet CLI by setting up the following Python virtual environment: +Keycard functionality is available to Wallet CLI by setting up the following Python virtual environment. The steps below can also be run via `keycard_wallet/wallet_with_keycard.sh`. ```bash # Install appropriate version of `keycard-py`. @@ -211,4 +211,27 @@ wallet auth-transfer send \ # Output: Keycard PIN: Transaction hash is 7d4c1b8e2f903a56fd19084b3c8b25d07e8f243829bc50addf6e2c78b4b09e45 +``` + +## Testing + +Tests for Keycard commands are in `keycard_wallet/tests/keycard_tests.sh`. Run from the repo root with a Keycard connected: + +```bash +bash keycard_wallet/tests/keycard_tests.sh +``` + +## SigningGroups + +`SigningGroups` (`wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved. + +Local signers are resolved and signed in pure Rust. Keycard signers store only their BIP32 key path; all of them are signed inside a single Python session (`connect` / `close_session`) when `sign_all` is called. The command calls `needs_pin` to decide whether to prompt for a PIN before signing. + +Foreign recipient accounts — those with no local key and no Keycard path — are silently skipped and require neither a signature nor a nonce. + +``` +SigningGroups { + local: [(AccountId, PrivateKey)], // signed in pure Rust + keycard: [(AccountId, BIP32Path)], // signed via a single Python/Keycard session +} ``` \ No newline at end of file diff --git a/keycard_wallet/tests/keycard_tests.sh b/keycard_wallet/tests/keycard_tests.sh new file mode 100755 index 00000000..e5ac2f2c --- /dev/null +++ b/keycard_wallet/tests/keycard_tests.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Run wallet_with_keycard.sh first + +source venv/bin/activate # Load the appropriate virtual environment + +export KEYCARD_PIN=111111 + +# Tests wallet keycard available +# - Checks whether smart reader and keycard are both available. +echo "Test: wallet keycard available" +wallet keycard available + +# Install a new mnemonic phrase to keycard +echo "Test: wallet keycard load" +export KEYCARD_MNEMONIC="fashion degree mountain wool question damp current pond grow dolphin chronic then" +wallet keycard load +unset KEYCARD_MNEMONIC + +echo "Test: wallet auth-transfer init --account-id \"m/44'/60'/0'/0/0\"" +wallet auth-transfer init --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet pinata claim --to \"m/44'/60'/0'/0/0\"" +wallet pinata claim --to "m/44'/60'/0'/0/0" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet auth-transfer init and send between two keycard accounts" +wallet auth-transfer init --account-id "m/44'/60'/0'/0/1" +wallet auth-transfer send --amount 40 --from "m/44'/60'/0'/0/0" --to "m/44'/60'/0'/0/1" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\"" +wallet account get --account-id "m/44'/60'/0'/0/1" + +# Send from keycard account to a local wallet account +echo "Test: create local wallet account" +LOCAL_ACCOUNT_ID=$(wallet account new public 2>&1 | grep -oP '(?<=Public/)\S+') +echo "Created local account: Public/${LOCAL_ACCOUNT_ID}" + +echo "Test: wallet auth-transfer init local account" +wallet auth-transfer init --account-id "Public/${LOCAL_ACCOUNT_ID}" + + +echo "Test: wallet auth-transfer send from keycard to local account" +wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/${LOCAL_ACCOUNT_ID}" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\"" +wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" + +# Create a local wallet account, fund it, and send to keycard account (co-signed: local key + keycard) + +echo "Test: wallet auth-transfer send from local account to keycard account" +wallet auth-transfer send --amount 10 --from "Public/${LOCAL_ACCOUNT_ID}" --to "m/44'/60'/0'/0/1" + +echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\"" +wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\"" +wallet account get --account-id "m/44'/60'/0'/0/1" + +# Send from keycard account to a local wallet account (foreign recipient — no signature needed) +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" + +echo "Test: wallet auth-transfer send from keycard to local account" +wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" diff --git a/keycard_wallet/wallet_with_keycard.sh b/keycard_wallet/wallet_with_keycard.sh new file mode 100755 index 00000000..bcd87bd3 --- /dev/null +++ b/keycard_wallet/wallet_with_keycard.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +cargo install --path wallet --force + +# Install appropriate version of `keycard-py`. +git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git keycard_wallet/python/keycard-py + +# Set up virtual environment. +python3 -m venv venv +source venv/bin/activate +pip install pyscard mnemonic ecdsa pyaes +pip install -e keycard_wallet/python/keycard-py \ No newline at end of file