feat: add offline one-sided transaction signing cucumber test#7743
feat: add offline one-sided transaction signing cucumber test#77430xPepeSilvia wants to merge 3 commits intotari-project:developmentfrom
Conversation
Add a complete integration test covering the full offline signing gRPC flow: create a view-only wallet, prepare a one-sided transaction for offline signing via gRPC, sign the transaction using exported spend keys (simulating an air-gapped signing device), broadcast the signed transaction via the view-only wallet, and verify the receiving wallet confirms the funds. New files: - OfflineSigning.feature: Cucumber scenario for the full flow - offline_signing_steps.rs: Step definitions for prepare, sign, broadcast Modified: - world.rs: Add offline_signing_prepared/signed state fields - steps/mod.rs: Register new step module Closes tari-project#7736 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces integration tests for the offline one-sided transaction signing flow. It adds new fields to the TariWorld struct to store transaction state, a Cucumber feature file outlining the full signing process, and the necessary step definitions to handle gRPC-based preparation, offline signing, and broadcasting. The review feedback suggests updating the Debug implementation for TariWorld to include the new fields for better observability and replacing a magic number with an enum variant in the gRPC request for improved readability.
| // Used for offline signing integration test — stores prepared and signed transaction JSON between steps. | ||
| pub offline_signing_prepared: Option<String>, | ||
| pub offline_signing_signed: Option<String>, |
There was a problem hiding this comment.
There was a problem hiding this comment.
Addressed in d77db9443: offline_signing_prepared and offline_signing_signed are now included in the Debug impl (lines 137–138 in the updated world.rs).
There was a problem hiding this comment.
Fixed in d77db9443: both offline_signing_prepared and offline_signing_signed fields are now included in the Debug impl.
| address: receiver_wallet_address, | ||
| amount, | ||
| fee_per_gram: fee, | ||
| payment_type: 2, // ONE_SIDED_TO_STEALTH_ADDRESS |
There was a problem hiding this comment.
Using the raw integer 2 for payment_type is a magic number. It is better to use the generated enum variant for improved readability and maintainability.
| payment_type: 2, // ONE_SIDED_TO_STEALTH_ADDRESS | |
| payment_type: grpc::payment_recipient::PaymentType::OneSidedToStealthAddress as i32, |
There was a problem hiding this comment.
Addressed in d77db9443: the payment_type field now uses PaymentType::OneSidedToStealthAddress as i32 with use grpc::{PaymentRecipient, payment_recipient::PaymentType} at the top of the file — no magic numbers remain.
There was a problem hiding this comment.
Fixed in d77db9443: the signing step was completely rewritten — the in-process key reconstruction is gone and replaced with kill → respawn-with-CLI-command → poll pattern.
SWvheerden
left a comment
There was a problem hiding this comment.
have you run the test and is it passing?
| let spend_wallet = SpendWallet::new(spend_key, view_key, None); | ||
| let wallet_type = WalletType::SpendWallet(spend_wallet); | ||
| let key_manager = | ||
| KeyManager::new(wallet_type).expect("Failed to create key manager for offline signing"); | ||
|
|
||
| // Parse the prepared transaction | ||
| let request = PrepareOneSidedTransactionForSigningResult::from_json(&prepared_json) | ||
| .expect("Failed to parse prepared transaction JSON"); | ||
|
|
||
| // Get consensus constants for LocalNet | ||
| let consensus_manager = ConsensusManager::builder(Network::LocalNet).build(); | ||
| let consensus_constants = consensus_manager.consensus_constants(0).clone(); | ||
|
|
||
| // Sign the transaction offline | ||
| let signed_result = sign_locked_transaction(&key_manager, consensus_constants, Network::LocalNet, request) | ||
| .expect("Offline signing failed"); | ||
|
|
||
| let signed_json = signed_result | ||
| .to_json() | ||
| .expect("Failed to serialize signed transaction"); |
There was a problem hiding this comment.
we need to use the wallet cli to sign this, not the struct, we need to test the complete cycle
There was a problem hiding this comment.
Fixed in d77db9443: the sign step now kills the wallet, respawns it with CliCommands::SignOneSidedTransaction as the boot-time command, and polls for the output file — exercising the full wallet CLI signing cycle.
| // Used for offline signing integration test — stores prepared and signed transaction JSON between steps. | ||
| pub offline_signing_prepared: Option<String>, | ||
| pub offline_signing_signed: Option<String>, |
| address: receiver_wallet_address, | ||
| amount, | ||
| fee_per_gram: fee, | ||
| payment_type: 2, // ONE_SIDED_TO_STEALTH_ADDRESS |
| # Step 1: View-only wallet prepares transaction for offline signing via gRPC | ||
| When I prepare an offline one-sided transaction of 100000 uT from wallet VIEW_WALLET to wallet WALLET_RECEIVER at fee 20 | ||
| # Step 2: Sign the prepared transaction offline using the exported spend key | ||
| Then I sign the prepared transaction offline using keys SENDER_KEYS |
There was a problem hiding this comment.
you should just use the spend wallet to sign
There was a problem hiding this comment.
Fixed in d77db9443: the feature now reads 'I sign the prepared transaction using wallet WALLET_SENDER' — the spend wallet signs via its own CLI subcommand, not reconstructed keys.
| let keys_json: serde_json::Value = | ||
| serde_json::from_str(&keys_content).unwrap_or_else(|e| panic!("Failed to parse keys JSON: {e}")); | ||
|
|
||
| let spend_key_hex = keys_json["spend_key"] |
There was a problem hiding this comment.
this is a public key, not private, it wont work
There was a problem hiding this comment.
Fixed in d77db9443: the in-process signing path that read spend_key from the exported keys file is gone — the wallet's own SignOneSidedTransaction CLI command is used instead, which reads the private key from its own encrypted database.
Rewrites the offline-signing step so it exercises the real wallet
binary's `SignOneSidedTransaction` CLI subcommand instead of
reconstructing a key manager in-process from exported keys — which was
both bypassing the code path the test is meant to validate and
misinterpreting `spend_key` from the exported-keys file as a private
key when it is actually the public spend key (so offline signing would
have failed outright once the view/spend key export format was fixed
to only emit private data for the view key).
Flow is now:
1. VIEW_WALLET prepares the transaction via gRPC
2. The prepared JSON is written to a file under WALLET_SENDER's temp
dir, WALLET_SENDER is killed, and respawned with
`command2 = SignOneSidedTransaction { input_file, output_file }`
so the console wallet signs the tx using its own in-database
spend key.
3. The signed JSON is polled off disk and passed back to
VIEW_WALLET which broadcasts it via gRPC.
Also replaces the magic-number `payment_type: 2` with
`PaymentType::OneSidedToStealthAddress as i32`, and adds
`offline_signing_prepared` / `offline_signing_signed` to the
`TariWorld` Debug impl so the state is visible in scenario dumps.
`SignOneSidedTransactionArgs` is re-exported from the console wallet
crate root alongside the other CLI arg types so integration tests can
construct the command without reaching into a private module.
Addresses review comments from @SWvheerden on tari-project#7743:
- offline_signing_steps.rs:141 "we need to use the wallet cli to sign
this, not the struct, we need to test the complete cycle"
- offline_signing_steps.rs:109 "this is a public key, not private, it
wont work"
- OfflineSigning.feature:27 "you should just use the spend wallet to
sign"
- world.rs:104 (Debug impl missing offline signing fields)
- offline_signing_steps.rs:64 (magic-number payment_type)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
this still has an unresolved gemini comment |
|
Both gemini comments are addressed in the current code:
Both were addressed in commit |
|
Hi @SWvheerden — the test was substantially rewritten in the latest commit ( What was wrong before What it does now
Both gRPC methods exist ( We have not been able to run the full cucumber integration suite locally due to infrastructure constraints, but the implementation follows the same |
|
All review comments addressed in Summary of what changed:
We have not been able to run the full cucumber integration suite locally (infrastructure constraint — requires a Linux CI host with compiled binaries). The test structure follows the same |
Summary
PrepareOneSidedTransactionForSigning, signs offline usingsign_locked_transactionwith the exported spend key, broadcasts viaBroadcastSignedOneSidedTransaction, and verifies the receiver has confirmed fundsCloses #7736
Test plan
OfflineSigning.featurescenario passes end-to-end🤖 Generated with Claude Code
Bounty payout
If this PR is accepted as a bounty submission, please direct funds to the following Tari wallet address:
123JbAxCZ4Vrh4jCjFLXTu4z8sLeggUR87fejwNunsB55NMBSH9RpkjNiw1WL2GkKr8JhqRZhbrsSREchGanchw2Nhb