Conversation
…--all Sending 1.99 QTC from an account holding exactly 2 QTC was refused right after mainnet enacted spec 153: Insufficient balance for send. Have: 2 QTC, Need: 2.000167025 QTC (estimated fee: 0.010167025 QTC) subxt's partial_fee_estimate calls TransactionPaymentApi_query_info at latest_finalized_block_ref. QPoW finality trails the head by ~100 blocks, so the estimate was computed by spec 152 -- the runtime the upgrade had just replaced -- and 153 cut FEE_SCALE tenfold. The real fee was ~0.001 QTC. The transfer was affordable; only the quote was stale. Estimate against the same block as every other read, via QuantusClient::partial_fee. Unify the finality choice. ExecutionMode::finalized already decided how long to wait for a transaction; it now also decides which block reads are taken at. Reads happen in ~80 places that have no reason to carry an ExecutionMode, so main publishes the flag once with ExecutionMode::install and QuantusClient::get_latest_block consults it -- one switch for waits and reads. wormhole_tip_block's own head/finalized branch and at_finalized_block go away, since get_latest_block already answers that question. --finalized-tx becomes --finalized (kept as an alias) because it no longer only governs transactions. Add send --all, which submits Balances::transfer_all and lets the chain deduct the exact fee. Sweeping an account by subtracting an estimate cannot be done reliably -- too low strands dust, too high is refused -- and --keep-alive chooses between reaping the account and leaving the existential deposit.
n13
left a comment
There was a problem hiding this comment.
Reviewer model: GPT-6 Sol
Verdict (advisory): Request changes
-
[P1] Keep proof reads aligned with the requested finality for library callers.
src/cli/wormhole.rs:1213-1216now selects its proof block throughget_latest_block(), whose global flag is installed only by the binary insrc/main.rs:92-98. The publichandle_wormhole_command(..., ExecutionMode { finalized: true, ... })andexecute_commandpaths do not install that flag. A library caller therefore waits for finalized transactions while building wormhole proofs from the unfinalized head; the prior code used the supplied mode to select the finalized block. Make the block selection honor the supplied mode outsidemainas well, and cover that public call path in a test. -
[P2] Make
system --runtimehonor the new global--finalizedread setting. The flag promises finalized reads, butsrc/cli/system.rs:184-220still gets runtime versions with no block hash and callschain_getHeaderandstate_getRuntimeVersionwith empty parameters.quantus --finalized system --runtimeconsequently reports the best block's height and runtime, including during the post-upgrade gap that motivates this PR. Resolve one selected block hash and use it for those runtime and header queries. -
[P3] Preserve fee reporting for
send --all.src/cli/send.rs:697-704passes the entire starting balance as the transfer amount, so the fee calculation atsrc/cli/send.rs:744-747always saturates to zero for a sweep and the verbose fee line disappears even though a fee was paid. Derive the transferred amount or fee from the included transaction before reporting it.
Validation: cargo +nightly fmt --all -- --check and git diff --check passed. The three new focused tests passed. With SKIP_CIRCUIT_BUILD=1, 364 library tests passed when the generated verifier artifact test was excluded; without that exclusion, that test failed because the skipped build did not create its required bins. The PR's full build/test and analysis CI jobs passed at head e87b9d7.
- Wormhole proof tips follow the ExecutionMode the flow is handed (QuantusClient::head_or_finalized). Library callers never install the switch, and the proof block must match the wait. - system --runtime reads the runtime version and header at one selected block, so --finalized moves both. - send reports the fee from the included transaction's MiningRewards::FeesCollected. A balance delta cannot separate it from what --all moved, and TransactionFeePaid omits the refund a reaped account forfeits. - The signing nonce is always read at the head. Under --finalized the finalized nonce made the extrinsic outdated whenever the account had anything unfinalized.
n13
left a comment
There was a problem hiding this comment.
Reviewer model: GPT-6 Sol
Verdict (advisory): Request changes
-
[P2] Keep the published finalized-block SDK helper.
src/lib.rs:62-66removes theat_finalized_blockre-export andsrc/cli/wormhole.rsdeletes the public function. It was explicitly exported for SDK use at the base commit, so existingquantus_cli::at_finalized_block(...)callers will stop compiling, although this PR does not change the crate's 2.3.0 version. Restore the helper and re-export it; it can usehead_or_finalized(true)internally. Also keep the survivingat_best_blockhelper pinned to the head: its newget_latest_block()call atsrc/cli/wormhole.rs:1477can return a finalized block afterExecutionMode::install(true). -
[P2] Preserve the public manual-nonce submission result.
src/cli/common.rs:709-716changessubmit_transaction_with_noncefromResult<H256>toResult<(H256, Option<H256>)>. Existing library callers expecting a transaction hash will fail to compile. Keep the old signature as a wrapper and add an inclusion-block variant forsend, as this PR already does for automatic-nonce submission.
The previous wormhole tip, system --runtime, and send fee-reporting findings are addressed at head e4f6a2b. Validation: cargo +nightly fmt --all -- --check, git diff --check, and cargo metadata --no-deps --locked passed. Local focused test compilation was stopped because this detached worktree has a cold target directory; the current-head Ubuntu/macOS build-and-test, Clippy, format, and security CI checks passed.
- Restore at_finalized_block and its lib.rs re-export, and pin at_best_block to the head. Both fetch the block they name via head_or_finalized, whatever --finalized installed. - submit_transaction_with_nonce returns the hash again. send uses the new submit_transaction_with_nonce_and_inclusion_block, mirroring submit_transaction and submit_transaction_with_inclusion_block.
Found live, minutes after mainnet enacted spec 153. Sending 1.99 QTC from an account holding exactly 2 QTC was refused:
The transfer was affordable. Only the quote was stale.
Cause
partial_fee_estimatecallsTransactionPaymentApi_query_infoatlatest_finalized_block_ref(subxttx_client.rs:583). QPoW finality trails the head, so the quote came from the runtime the upgrade had just replaced — and 153 cutFEE_SCALEtenfold. Measured on mainnet at the time:Real fee ~0.001 QTC; quoted 0.0102.
1. Estimate at the block everything else reads
QuantusClient::partial_feeruns the same runtime API atget_latest_block(). Replaces bothpartial_fee_estimatecall sites (send.rs,cold_signing.rs).2. One switch for finality
An audit of every
finalizedreference found those two sites were the only wrong ones — everything else is a deliberate--finalized-txopt-in, or already fixed by #152 (collect-rewards proofs) and #154 (client metadata). So rather than add a parallel mechanism, this unifies what exists.ExecutionMode::finalizedalready decided how long to wait for a transaction. It now also decides which block reads are taken at. Reads happen in ~80 places with no reason to carry anExecutionMode, somainpublishes the flag once viaExecutionMode::install, andget_latest_blockconsults it — one switch governs waits and reads, the waywait_for_transactionalready governs waiting.--finalized-txbecomes--finalized(kept as a clap alias) since it no longer governs only transactions.system --runtimereads the runtime version and header at one selected block, so the flag moves both.Some reads deliberately don't follow the installed switch:
ExecutionModethe flow is handed, viaQuantusClient::head_or_finalized. The proof block must match the wait, and library callers reachinghandle_wormhole_commandnever install the switch.at_best_blockandat_finalized_blockfetch the block they name.Default is the head.
--finalizedopts back in for callers who want finality's guarantees and will wait ~20 minutes for them.3.
send --allSweeping by subtracting an estimate cannot be made reliable — too low strands dust, too high is refused, which is exactly what happened above.
--allsubmitsBalances::transfer_alland lets the chain deduct the exact fee.--keep-alivechooses between reaping the account and leaving the existential deposit.--amountand--allare mutually exclusive, enforced by clap.The verbose fee line reads
MiningRewards::FeesCollectedfrom the included transaction. A balance delta can't separate the fee from what--allmoved, andTransactionFeePaidomits the refund a reaped account forfeits: on a dev chain it reported 0.008028025 for a sweep that cost 0.008203025.sendneeds the inclusion block for this, so manual-nonce submission gainssubmit_transaction_with_nonce_and_inclusion_block;submit_transaction_with_noncestill returns the hash.Verification
--allbuildsBalances::transfer_all, and a fixed amount still buildstransfer_allow_deathSKIP_CIRCUIT_BUILD=1 cargo clippy --all-targets --locked -- -D warningscleancargo +nightly fmt --all -- --checkclean--finalized system --runtimereports the finalized block, ~100 behind the head--all --keep-aliveand a reaping--all.--finalized sendfrom an account with an unfinalized transaction now signs with the head nonce and is included; it was rejected as outdated beforeFollow-up, not in this PR
On a dev node the pool never reports
InFinalizedBlockfor a watched transaction: a--finalizedwait ends inDropped("Finality timeout")after 480 s even though its block is finalized, so the command reports failure for a finalized transfer. Predates this PR —--finalized-txshared the wait.COMPATIBLE_RUNTIMESstill tops out at 149, so every mainnet command printsspecVersion=153 … newer than this CLI's tested list. Cosmetic —transaction_versionnever moved off 6 — but all three chains are on 153 now and it should be bumped.