Skip to content

CLI: Add --sign-only with durable nonce for transaction submit - #71

Open
mcintyre94 wants to merge 1 commit into
mainfrom
submit-sign-only
Open

mcintyre94 wants to merge 1 commit into
mainfrom
submit-sign-only

Conversation

@mcintyre94

Copy link
Copy Markdown
Member

The current version of transaction submit requires the fee payer and all forwarded signers to be available as signers at the time of the call.

This is unsuitable for migration use cases where, for example, the keypair transferring its authority to a PDA may need to sign offline.

This PR adds a durable nonce option for the relay transaction, and a --sign-only mode similar to other Solana CLI commands. Using a durable nonce instead of a recent blockhash as the relay transaction's lifetime lets each signer build and sign the same relay transaction independently, without RPC calls. Under --sign-only, --fee-payer and --durable-nonce-authority also accept an address, whose signature is reported as absent, and --dump-transaction-message prints the relay transaction message so runs can be compared.

Without --sign-only, --durable-nonce submits as usual with all relay signers local, checking the durable nonce value and authority first.

The output is CliSignOnlyData, matching other Solana CLI --sign-only outputs. This PR only addresses offline signing. A follow-up PR will let submit accept these relay signatures in place of relay signers.

Note some intentional differences from similar CLIs:

  • --fee-payer is required when using --sign-only, instead of defaulting to the configured keypair, which would differ between
    offline signers
  • --durable-nonce-authority defaults to the fee payer rather than the configured keypair, for the same reason
  • we use --durable-nonce[-authority|-value] rather than --nonce and --nonce-authority, because the relay uses a System Program nonce account, distinct from the SPL nonce args (--nonce-account, --nonce-authority) on other commands
  • we use --durable-nonce-value to pass the nonce value, instead of --blockhash. submit has no --blockhash arg, and nonce value is clearer

Stack created with GitHub Stacks CLI • Give Feedback 💬

@mcintyre94
mcintyre94 added this pull request to stack #72 October 1, 2026 16:14
@mcintyre94 mcintyre94 changed the title Add --sign-only with durable nonce for transaction submit CLI: Add --sign-only with durable nonce for transaction submit Oct 1, 2026

@joncinque joncinque left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks great overall! Just a few points which I hope will simplify the code

Comment on lines +63 to +66
/// Durable nonce value used as the relay transaction's blockhash. Read from the durable nonce
/// account when omitted.
#[clap(long, value_name = "HASH", requires = "durable-nonce")]
durable_nonce_value: Option<Hash>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: value is a bit vague, especially when this is supposed to be the nonce hash. At the very least, let's go with durable_nonce_hash. I do think we might want to prefer blockhash to be consistent with the other tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've switched this to --blockhash. If used without --durable-nonce then it can be used to set the blockhash, instead of fetching the latest one. This aligns with our other tools

Comment thread clients/cli/src/commands/transaction/submit.rs
Comment thread clients/cli/src/commands/transaction/submit.rs Outdated
Comment thread clients/cli/src/commands/transaction/submit.rs Outdated
Comment thread clients/cli/src/cli.rs
Comment on lines +78 to +79
/// `transaction submit --sign-only` also accepts an address, whose signature is collected
/// separately. Defaults to the configured keypair.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm torn about whether this comment is needed, but I guess it doesn't hurt. I did get confused about null signers back in the day 😅

Comment thread clients/cli/src/commands/transaction/submit.rs Outdated
Comment thread clients/cli/src/commands/transaction/submit.rs Outdated
Comment thread clients/cli/src/commands/transaction/submit.rs
Comment thread clients/cli/src/commands/transaction/submit.rs Outdated
Comment thread clients/cli/src/client.rs
@mcintyre94
mcintyre94 force-pushed the submit-sign-only branch 3 times, most recently from 06d16f1 to 9d94971 Compare October 6, 2026 16:23
Base automatically changed from submit-double-sign to main October 6, 2026 17:27
@mcintyre94
mcintyre94 force-pushed the submit-sign-only branch 2 times, most recently from 5920cbf to 5fa4318 Compare October 7, 2026 17:05
@mcintyre94
mcintyre94 requested a review from joncinque October 7, 2026 17:42

@joncinque joncinque left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks great! Just the point on the TODO really matters, the rest can be handled separately if you choose

Comment thread clients/cli/src/commands/transaction/submit.rs Outdated
Comment thread clients/cli/src/client.rs Outdated
let test = SubmitTest::new(env, &signer).await;
// The forwarded signer also authorizes the durable nonce. A separate durable nonce authority
// would push this relay transaction over the transaction size limit.
// TODO: Test a separate durable nonce authority once the relay transaction is v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rather than putting a TODO, let's create an issue for this or add it to an existing issue

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added it to the tracking issue with a permalink to this test

Comment on lines +682 to +700
#[test]
fn sign_only_requires_authority_signatures() {
let env = SubmitTestEnv::new();
assert_failure(
&env.submit_message(&[
"--fee-payer",
&env.keypair_file(&env.fee_payer),
"--durable-nonce",
&env.durable_nonce.to_string(),
"--blockhash",
&env.durable_nonce_value.to_string(),
"--sign-only",
]),
&format!(
"missing signature for authority {}, authorities sign with `transaction sign`",
env.authority.pubkey()
),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just to make sure I understand, this is check to make sure that all authorities are explicitly passed, even as null signers, to the sign only command. Is that correct?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nope this is in submit, so the requirement is that we have a signature for all of the authorities on the authorization message. Ie we have a signature for the authority of all the PDAs that will be promoted. Those signatures from transaction sign become part of the Execute instruction in the relay transaction, so we must have them all before we call transaction submit. If any are missing then the transaction would fail, and if you pass different ones in different calls to transaction submit then you'd get different relay transactions. So this is saying that we must have all those signatures when we call transaction submit, even if it's with --sign-only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ohhh gotcha, that makes sense, thanks for the explanation!

The current version of `transaction submit` requires the fee payer and
all forwarded signers to be available as signers at the time of the
call.

This is unsuitable for migration use cases where, for example, the
keypair transferring its authority to a PDA may need to sign offline.

This PR adds a `--sign-only` mode similar to other Solana CLI commands.
`--blockhash` can be used to set the relay transaction's lifetime.
Combined with `--durable-nonce` and `--durable-nonce-authority`, it can
be set to the current value of a durable nonce, so that signatures don't
expire.

Under `--sign-only`, `--fee-payer`
and `--durable-nonce-authority` also accept an address, whose signature
is reported as absent, and `--dump-transaction-message` prints the
relay transaction message so runs can be compared.

Without `--sign-only`, `--durable-nonce` submits as usual with all relay
signers local, checking the durable nonce value and authority first.

The output is `CliSignOnlyData`, matching other Solana CLI `--sign-only`
outputs. This PR only addresses offline signing. A follow-up PR will
let `submit` accept these relay signatures in place of relay signers.

Note some intentional differences from similar CLIs:

- we use `--durable-nonce[-authority]` rather than `--nonce` and
  `--nonce-authority`, because the relay uses a System Program nonce
  account, distinct from the SPL nonce args (`--nonce-account`,
  `--nonce-authority`) on other commands

@joncinque joncinque left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks great!

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.

2 participants