Skip to content

Validate PartiallySignedTransaction after decoding - #2093

Open
oliv3rdrt wants to merge 2 commits into
mintlayer:masterfrom
oliv3rdrt:validate_partially_signed_tx
Open

Validate PartiallySignedTransaction after decoding#2093
oliv3rdrt wants to merge 2 commits into
mintlayer:masterfrom
oliv3rdrt:validate_partially_signed_tx

Conversation

@oliv3rdrt

Copy link
Copy Markdown
Contributor

Problem

Wallet RPC methods and CLI commands that accept a PartiallySignedTransaction decode it from bytes with SCALE, which skips the checks that PartiallySignedTransaction::new performs. A malformed but still decodable transaction (for example one whose witness count does not match the number of inputs) can therefore reach the wallet and make it panic instead of returning an error.

Fix

Call ensure_consistency immediately after decoding in GenericTransaction::decode_from_untagged_bytes, the shared path used by account_sign_raw_transaction and transaction_inspect (and the CLI commands that call them). A new GenericTransactionError::InconsistentPartiallySignedTransaction variant is returned on failure.

The Basic consistency level is used on purpose: it enforces the structural invariants that prevent the panics, while still accepting partial transactions that are legitimately incomplete during signing. Using WithAdditionalInfo here would reject those and also break the existing decode test.

Tests

Added a unit test that feeds a hand-encoded, inconsistent PartiallySignedTransaction through the decode path and asserts it is rejected. The existing tests still pass and clippy is clean for the changed crate.

Closes #2069

@erubboli

Copy link
Copy Markdown
Member

Thanks for this — the approach looks right to me. I checked that Basic is genuinely sufficient for the panics you're targeting: every reachable out-of-bounds index on a decoded PSTx (wallet-controller/src/lib.rs:1264, :1278, and the three signers) is covered by the witnesses/input_utxos/destinations/htlc_secrets == inputs.len() invariant, and the sighash-commitment path and verify_tx_signature are Result-based with no indexing. I also confirmed nothing in tree produces a PSTx that would now fail to decode — every producer goes through new/new_for_wallet with at least Basic.

A few small things:

  1. Both callers (wallet-rpc-lib/src/rpc/mod.rs:913 and :1118) do .map_err(|_| RpcError::InvalidRawTransaction), so the new InconsistentPartiallySignedTransaction(..) payload is discarded. A user with, say, a wrong destinations count gets the generic "invalid raw transaction", indistinguishable from a hex/encoding error — the detail you added never reaches them. Worth a dedicated RpcError variant carrying the inner error.

  2. The new comment says WithAdditionalInfo is skipped so that a transaction still being assembled "may not carry all of its input utxos" — that part is misleading: Basic does enforce input_utxos.len() == inputs.len(), only the entries may be None. Only the additional_info half of the sentence holds.

  3. Optional: the new error field has no #[from]/#[source], so Error::source() returns None for it. Cosmetic, since the message still interpolates.

Unrelated to this diff, but the same bug class as #2069 if you feel like folding it in: wallet-rpc-client/src/rpc_client/client_impl.rs:1386 does decode_all(...).expect("valid partially signed tx") after hex::decode(...).expect("valid hex"), so a malformed response from a remote wallet-rpc server panics the CLI. This PR hardens the server-side ingress and leaves the client side unguarded.

One process note: this branch is based on 6dce99e4, which predates the CI fix from #2102 (Ledger app repo revision, pinned ledger-app-dev-tools). Could you rebase onto current master so CI has a chance of passing once the workflow run is approved? The same applies to #2094, #2095 and #2096#2097#2100 are already based on a commit that includes it.

Wallet RPC methods and CLI commands accept a hex-encoded
PartiallySignedTransaction and decode it with SCALE, which bypasses the
invariants that PartiallySignedTransaction::new enforces. A malformed but
still decodable transaction could then make the wallet panic, e.g. on a
witness/input count mismatch.

Call ensure_consistency right after decoding in
GenericTransaction::decode_from_untagged_bytes, the shared decode path used by
account_sign_raw_transaction and transaction_inspect, so such input is rejected
with a proper error instead. The Basic level is used so that transactions that
are still being assembled are not rejected.
Both callers of decode_from_untagged_bytes mapped the error to a unit
InvalidRawTransaction variant, so the reason a transaction was rejected never
reached the user and an inconsistent transaction looked the same as malformed
hex. Give the variant the inner error instead, so the message says which count
did not match.

Also correct the comment above the Basic check. Basic does verify that the
input utxo count matches the input count, the entries are just allowed to be
None, so only the additional info part of the original wording was right.

Add #[source] to the new error field so Error::source() returns the inner
error rather than None.
@oliv3rdrt
oliv3rdrt force-pushed the validate_partially_signed_tx branch from 3f68397 to fbf262e Compare August 28, 2026 13:34
@oliv3rdrt

oliv3rdrt commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@erubboli Rebased all four onto master.

I folded the error into the existing InvalidRawTransaction variant rather than adding a second one, since the old unit variant would have been left unused. Comment is fixed too, you were right that Basic does check the input utxo count and only the entries can be None. Added the #[source] as well.

Left the client side expects for a follow-up against #2069. There are two of them there rather than one.

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.

Wallet should validate PartiallySignedTransaction

2 participants