Repository navigation
fix(key-wallet): cap asset locks at Core's 100-input / 20 480-byte standard limits - #1191
romchornyi wants to merge 10 commits into
Conversation
Dash Core refuses an asset lock with more than 100 inputs as non-standard (MAX_STANDARD_ASSET_LOCK_INPUTS, `assetlocktx-too-many-inputs`) and peers drop it silently; Platform enforces the same bound in consensus. The asset-lock builder had no input cap, so a drain over an account with more than 100 UTXOs (e.g. a heavily mixed CoinJoin account) built a transaction the network could never accept, and the wallet only saw the broadcast time out. - Add MAX_ASSET_LOCK_INPUTS (100) and MAX_ASSET_LOCK_TX_SIZE (20480). - CoinSelector::with_max_inputs / TransactionBuilder::set_max_inputs: a drain becomes a bounded drain that spends the N largest UTXOs and leaves the rest; other strategies fall back to largest-first when their pick is over the cap, and the builder fails with BuilderError::TooManyInputs when no selection within the cap funds the amount. A builder carrying an asset-lock payload is bounded at MAX_ASSET_LOCK_INPUTS automatically; regular builds are unchanged. - Check the signed asset lock against both limits after the build and release the reservation owner-guarded on failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bounded drain over 104 and 103 UTXOs (soft wallet and external signer) takes exactly the 100 largest and reserves only them; a drain of exactly 100 is unchanged; an exact-amount lock that needs more than 100 inputs fails with BuilderError::TooManyInputs and reserves nothing; one that fits still builds; the post-build standardness backstop rejects over-cap and oversized transactions. Coin-selector tests cover the bounded drain, tie determinism, the largest-first fallback and the reported input need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_max_inputs(0) bounded coin selection at one input but the post-selection check at zero, so every build failed with TooManyInputs. Clamp both alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AssetLockResult gains remaining_utxo_count / remaining_duffs: what a bounded drain left behind (0/0 for a full drain or an exact build), so a partial drain is no longer indistinguishable from a full one. - Bounded selection also falls back to largest-first when the first strategy reports insufficient funds, not only when its pick is over the cap. - Oversized asset locks fail with the typed BuilderError::AssetLockTooLarge, checked against the signed-size upper bound inside the builder before anything is reserved or signed; the post-sign backstop is gone. - Move MAX_ASSET_LOCK_INPUTS / MAX_ASSET_LOCK_TX_SIZE beside MAX_STANDARD_TX_INPUTS and re-export them from asset_lock_builder, removing the circular module import; drop the redundant explicit set_max_inputs in the asset-lock builder (the payload-derived bound applies). - Clamp the input cap once (input_cap -> usize); CoinSelector::with_max_inputs takes the bound as given. - Bounded drain partitions with select_nth_unstable_by instead of sorting. - Docs: the cap makes a lock relayable by Core and within Platform's input cap, not every state transition small enough (InstantSend proof size, use a ChainLock proof); a bounded drain leaves the smallest UTXOs, so a follow-up drain may fall below the transition's minimum amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Re-check the signed asset lock's actual size after signing (an external signer may return larger scriptSigs than the pre-sign bound assumes) and release the reservation owner-guarded on BuilderError::AssetLockTooLarge. - Size-aware fallback: an exact-amount asset lock that fits the input cap but not the size limit retries largest-first before failing. - Derive remaining_utxo_count / remaining_duffs from the candidates the builder actually offered selection (new crate-private build_signed_reserved_reporting) instead of re-filtering the accounts. - Remove the unused TransactionBuilder::set_max_inputs: only an asset-lock payload bounds selection, so a regular drain never silently leaves coins. - Compute is_asset_lock once in assemble_unsigned. - Coin selection: one call path for LargestFirst, and no largest-first retry when the spendable pool is below even a one-input spend. - Fix the stale add_funding_reservation_only doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A signer returning oversized scriptSigs is caught after signing with AssetLockTooLarge and the reservation is released. - An exact-amount asset lock whose smallest-first pick fits the input cap but not the size limit falls back to largest-first. - A bounded selection over a pool below a one-input spend reports the first strategy's InsufficientFunds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An asset-lock drain through the public TransactionBuilder over 100
candidates now fails with BuilderError::TooManyInputs { max: 100 }. The
bounded "largest N" drain needs the crate-private bounded_drain() flag, set
only by the asset-lock builder, the one path that reports what was left
behind (remaining_utxo_count / remaining_duffs). Unselected is computed only
for that drain.
- One signed-size upper bound (signed_size_upper_bound, from the assembled
transaction) drives both the size-aware fallback and the pre-sign check.
- A bounded drain whose largest N exceed MAX_ASSET_LOCK_TX_SIZE drops its
smallest inputs until the bound fits instead of failing.
- The builder skips its largest-first size retry when selection already ran
or fell back to largest-first (CoinSelector::select_coins_reporting).
- The post-sign actual-size check moves into the builder's signed path, so
every signed asset-lock build is covered; the builder releases its own
reservation owner-guarded on that error.
- CoinSelector::with_max_inputs treats 0 as 1; doc no longer claims a builder
clamp.
- Selection is assembled through unsigned_from_selection so a retry does not
consume the builder state.
- Move a test inserted inside another test's doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in shrink
- The asset-lock size bound prices every input as a P2PKH spend, so an
asset-lock build now refuses a non-P2PKH candidate up front with the typed
BuilderError::AssetLockNonP2pkhInput { outpoint }, before anything is
selected or reserved. build_unsigned_reserved documents that an external
signer must keep standard 107-byte P2PKH scriptSigs (no post-sign check on
that path). Test UTXO dummies now carry their address's P2PKH script.
- Bounded-drain shrink: each dropped input lowers the bound by exactly
TX_INPUT_SIZE, so compute the kept count once and reselect once; otherwise
fail with AssetLockTooLarge for the transaction actually checked.
- Docs: remaining_* include inputs dropped by the cap or the size shrink and
must be read to learn about a partial drain; both build_asset_lock and
build_asset_lock_with_signer are bounded-drain paths; an InstantSend proof
for a lock near the cap may exceed Platform's transition size limit (use a
ChainLock proof, cf. dashpay/platform#5460).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y errors - Asset-lock builds no longer refuse when any candidate is non-P2PKH: such outputs are simply not offered to selection (one odd coin must not block every asset lock from the account). The unreleased AssetLockNonP2pkhInput variant is dropped. - Size retries (bounded-drain shrink, exact-amount largest-first) propagate a failed reselection's real error, and AssetLockTooLarge reports the smallest transaction actually checked. - unsigned_from_selection takes outputs and payload by value: the single-pass build moves them; a retry reuses the payload of the transaction it rejects. - Remove CoinSelector::select_coins_reporting; the builder skips its largest-first retry only when LargestFirst is the configured strategy. - remaining_* docs say exactly what is counted; bounded_drain documents that only the reporting signed path may be used with it. - One canonical description of the asset-lock limits and bounded drain on build_asset_lock, linked from the constants, the builder flag and add_funding_reservation_only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cap check - Replace the 0 sentinel for "not an asset lock" with an Option gated on is_asset_lock. - Fold the first pick's and the retry's TooManyInputs check into one select_within_cap helper. - Document that an exact-amount asset lock never drops a change output to fit the size limit (change is only emitted above dust, so dropping it would burn that value as fee). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post |
|
Bots are done — your move: post |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #1191 +/- ##
==========================================
+ Coverage 78.86% 79.05% +0.19%
==========================================
Files 304 304
Lines 81381 82126 +745
==========================================
+ Hits 64178 64924 +746
+ Misses 17203 17202 -1
|
…t, not the standard-tx limit An asset lock may carry at most `max_asset_lock_transaction_inputs` (100) inputs: Dash Core's relay policy drops a larger lock silently (`assetlocktx-too-many-inputs`) and Platform consensus validates the same bound. The drain only inherited key-wallet's 500-input standard-transaction limit, so a 101–500-input balance built and broadcast a lock that could never land (seen on mainnet in support case 32500). Read the cap from the protocol version, judge the built drain payload against it in the same abandon path as the drain floor (reservation released, nothing on the wire) and refuse with the typed `AssetLockTooManyInputs`; have the preview report `InputLimitMayBeExceeded` above it before sizing the payload. Tests, registry and host fixture texts follow. Scoped to drains; dashpay/rust-dashcore#1191 brings the same bound to every asset-lock build in key-wallet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ZocoLini
left a comment
There was a problem hiding this comment.
1400 lines for somehting that we already have implemented for a different type of transaction its too much, i think this can reuse what we already have for the 500-input cap instead of adding new machinery:
- Make the existing TooManyInputs check use a per-type limit: 100 for asset locks, 500 otherwise.
- For the drain, seed the 100 largest coins with add_inputs + add_funding_reservation_only, so the build only spends those.
- If exact-amount selection picks more than the limit, retry largest-first.
100 P2PKH inputs stay well under 20 480 bytes, so the size checks aren't needed, and the remaining-funds fields can go: the caller can read them from the balance. That should be a few dozen lines plus a test.
|
Closing in favour of a wallet-side fix: the app consolidates CoinJoin coins before the drain asset lock (dashpay/dashwallet-ios#1221), the same approach as the Android wallet. The underlying SDK gap stays tracked in #1190. |
Fixes #1190.
A wallet whose CoinJoin account holds more than 100 coins cannot shield. Every CoinJoin → Shielded attempt builds an asset lock that spends every mixed UTXO. Dash Core (v23.1.7+, and the v24 line) refuses an asset lock with more than 100 inputs as non-standard (
IsStandardSpecialTx:MAX_STANDARD_ASSET_LOCK_INPUTS{100}→assetlocktx-too-many-inputs; above 20 480 bytes →assetlocktx-too-big). Peers drop such a lock without a reject, so the wallet only ever sees a 60 sUncertainbroadcast. Platform enforces the same 100-input cap in consensus (max_asset_lock_transaction_inputs, dashpay/platform#3491). key-wallet applied only the regular-transaction cap of 500 (MAX_STANDARD_TX_INPUTS) to asset locks. On mainnet this blocked a user with 104 mixed coins for a month (support case 32500); submitting the lock through DAPI returnedassetlocktx-too-many-inputs.The limits.
MAX_ASSET_LOCK_INPUTS = 100andMAX_ASSET_LOCK_TX_SIZE = 20_480live next toMAX_STANDARD_TX_INPUTSintransaction_builderand are re-exported fromasset_lock_builder. AnyTransactionBuilderbuild that carries an asset-lock payload is held to them:BuilderError::TooManyInputs { count, max: 100 }before anything is reserved, withcountthe fewest inputs the amount actually needs.BuilderError::AssetLockTooLarge { size, max }.Bounded drain.
build_asset_lock/build_asset_lock_with_signerin drain mode no longer spend every final UTXO:AssetLockResultfields,remaining_utxo_countandremaining_duffs. They are 0 for a full drain and for exact-amount builds, and they count only final, unreserved, spendable P2PKH candidates.TransactionBuilderis not bounded. Over 100 candidates it returnsTooManyInputs, so no caller gets a partial drain without being told.Not in scope.
remaining_*to report a partial move (platform-wallet: asset-lock drains are not bounded bymax_asset_lock_transaction_inputs, so CoinJoin → Shielded never completes for wallets with more than 100 mixed coins platform#5478).build_asset_lock.Testing.
cargo test -p key-wallet --all-features: 709 passed.cargo test -p key-wallet-ffi: 277 passed.clippy --all-features --all-targets -D warningsandcargo doc -D warningsare clean.🤖 Generated with Claude Code
Summary by CodeRabbit