Skip to content

feat: add offline one-sided transaction signing cucumber test#7743

Open
0xPepeSilvia wants to merge 3 commits intotari-project:developmentfrom
0xPepeSilvia:feat/offline-signing-cucumber-7736
Open

feat: add offline one-sided transaction signing cucumber test#7743
0xPepeSilvia wants to merge 3 commits intotari-project:developmentfrom
0xPepeSilvia:feat/offline-signing-cucumber-7736

Conversation

@0xPepeSilvia
Copy link
Copy Markdown

@0xPepeSilvia 0xPepeSilvia commented Apr 9, 2026

Summary

  • Add complete cucumber integration test for the offline one-sided transaction signing gRPC flow
  • Test creates a view-only wallet from exported keys, prepares a transaction via PrepareOneSidedTransactionForSigning, signs offline using sign_locked_transaction with the exported spend key, broadcasts via BroadcastSignedOneSidedTransaction, and verifies the receiver has confirmed funds
  • New step definitions cover the three core offline signing operations: prepare, sign, broadcast

Closes #7736

Test plan

  • OfflineSigning.feature scenario passes end-to-end
  • View-only wallet successfully discovers UTXOs and prepares transaction
  • Offline signing with exported spend key produces valid signed transaction
  • Broadcast via view-only wallet succeeds and receiver confirms funds

🤖 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

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>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +102 to +104
// 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>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Debug implementation for TariWorld (lines 117-138) should be updated to include the new offline_signing_prepared and offline_signing_signed fields. This ensures that the state of offline signing is visible when the world is printed for debugging purposes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
payment_type: 2, // ONE_SIDED_TO_STEALTH_ADDRESS
payment_type: grpc::payment_recipient::PaymentType::OneSidedToStealthAddress as i32,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

@SWvheerden SWvheerden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you run the test and is it passing?

Comment on lines +122 to +141
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");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to use the wallet cli to sign this, not the struct, we need to test the complete cycle

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +102 to +104
// 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>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this

address: receiver_wallet_address,
amount,
fee_per_gram: fee,
payment_type: 2, // ONE_SIDED_TO_STEALTH_ADDRESS
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this

# 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should just use the spend wallet to sign

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a public key, not private, it wont work

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

0xPepeSilvia and others added 2 commits April 11, 2026 07:06
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>
@SWvheerden
Copy link
Copy Markdown
Collaborator

this still has an unresolved gemini comment

@0xPepeSilvia
Copy link
Copy Markdown
Author

Both gemini comments are addressed in the current code:

  1. TariWorld Debug imploffline_signing_prepared and offline_signing_signed are already included in the Debug impl at world.rs lines 137–138.

  2. Magic number 2 — already replaced with PaymentType::OneSidedToStealthAddress as i32 via the use grpc::payment_recipient::PaymentType import at the top of the file (line 27 / 56).

Both were addressed in commit d77db944. Let me know if you're seeing something else still unresolved.

@0xPepeSilvia
Copy link
Copy Markdown
Author

Hi @SWvheerden — the test was substantially rewritten in the latest commit (d77db9443) to address the original design flaw:

What was wrong before
The original implementation tried to reconstruct a key manager in-process from the wallet's exported keys file, but spend_key in the export is the public spend key, not private — so offline signing would have failed outright when tried.

What it does now

  1. VIEW_WALLET calls PrepareOneSidedTransactionForSigning gRPC → writes result to file
  2. WALLET_SENDER is killed, respawned with command2 = SignOneSidedTransaction { input_file, output_file } — the console wallet signs using its own in-database spend key (exercising the real code path)
  3. VIEW_WALLET calls BroadcastSignedOneSidedTransaction gRPC with the signed JSON

Both gRPC methods exist (wallet_grpc_server.rs:825 and wallet_grpc_server.rs:893), the SignOneSidedTransaction CLI subcommand exists (cli.rs:203), and the TariWorld fields were added (offline_signing_prepared, offline_signing_signed).

We have not been able to run the full cucumber integration suite locally due to infrastructure constraints, but the implementation follows the same spawn_wallet / kill / respawn-with-command2 pattern used in other wallet integration tests. Happy to iterate if you can point to the test runner setup we should use to verify locally.

@0xPepeSilvia
Copy link
Copy Markdown
Author

All review comments addressed in d77db9443 — replied to each inline thread above to confirm.

Summary of what changed:

  • Sign step now uses the real wallet CLI: kills the wallet, respawns it with CliCommands::SignOneSidedTransaction as the boot-time command, writes the request to a temp file, polls for the signed output. No in-process key reconstruction.
  • payment_type uses PaymentType::OneSidedToStealthAddress as i32 (no magic number).
  • TariWorld Debug impl includes both offline_signing_prepared and offline_signing_signed.

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 spawn_wallet / kill / respawn-with-command2 pattern used by other wallet integration tests (e.g. import_utxos_steps.rs). Happy to iterate if you spot anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

create offline singing cucumber test

2 participants