Merge chain-private main into chain - #675
Conversation
Unauthenticated TCP connections could exhaust fds and turn EMFILE into permanent transport death. Count pending inbound handshakes, back off on transient accept errors, and isolate a single transport ending. Co-authored-by: Cursor <cursoragent@cursor.com>
Hash-derived author and aggregator addresses have no signing key, so a credit without a leaf is permanently frozen. Pair increase_balance with record_transfer on every wormhole credit path. Co-authored-by: Cursor <cursoragent@cursor.com>
Swallowing ConnectionAborted without a backoff left the listener Pending with no waker. Always back off, iterate from a stable poll index, and replace never-looping while-lets that clippy rejects. Co-authored-by: Cursor <cursoragent@cursor.com>
fix: keep litep2p listening after transient accept errors
…dits stay exitable Fee shares computed in planck can be sub-quantum; hash_leaf then commits 0 and freezes the credited balance on a keyless address. Split the already-quantized fee in whole quanta (burn rounds up, rebate rounds down) and burn any remainder. Co-authored-by: Cursor <cursoragent@cursor.com>
fix: record zk-tree leaves for wormhole miner fee and aggregator rebate
* fix: reject below-ED transfers that would reap the sender The deposit gate checked amount+dust while the credit wrote only amount, so a reapable sender could lose funds to a dead dest and desync total issuance. Co-authored-by: Cursor <cursoragent@cursor.com> * test: pin that transfer debit matches the amount gate Balances returns the requested amount on a reap, and a larger-debit mock that passes can_deposit(amount) credits the full debit instead of silently dropping it. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: drop the mining-reward treasury split for a genesis endowment The treasury is funded once at genesis with half of max supply instead of taking an ongoing share of each block reward. Miners receive 100% of emission; set_treasury_portion is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: stop pre-funding the treasury in genesis presets Treasury balances will come from a later endowment list. Presets still configure the treasury account; they no longer mint it half of max supply. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: align miner payouts to the wormhole leaf quantum ZK-tree leaves commit amount/10^10, so raw planck credits left an unexitable remainder on keyless miner addresses. Combine fees and emission, floor once, and hold dust or a missing author in CollectedFees for the next miner — never treasury. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…owns it (#3) `do_schedule_inner` and `do_schedule_named_inner` add a preimage reference conditionally on success (`lookup_hash.filter(|_| request_preimage)`) but dropped it unconditionally on every failure path. `request()` runs only after `place_task` succeeds, so a failed invocation has acquired no reference of its own and its drop consumed the caller's instead. Referenda reaches this through the `schedule::v3` shim with `request_preimage = true`, holding exactly one reference between `submit` and `conclude_ongoing`. A single failed enactment-scheduling attempt against a full agenda therefore unpinned an approved proposal's call data: the noter's ordinary `unnote_preimage` deposit reclaim could then delete the bytes, and the enactment was discarded as `CallUnavailable` — terminal, with no retry path and an event indistinguishable from routine cleanup. Debug builds trip pallet-preimage's own `do_unrequest_preimage failed - counter underflow?` assertion, which release builds compile out. Gate the failure-path drops on ownership in both functions. The dispatchables still pass `request_preimage = false` when their own `bound()` created the note, so the state-bloat cleanup that motivated the unconditional drop is preserved. Adds `failed_schedule_keeps_a_preimage_reference_it_does_not_own` as the regression test, plus `successful_schedule_adds_its_own_preimage_reference` to pin the other half of the ownership contract.
#9) * fix(qpow): verify PoW seal in the import-queue verifier so bad blocks penalise the peer The import-queue verifier (formerly SimplePowVerifier) only popped the seal and never checked the proof-of-work; the check lived in import_block and ran after a full check_inherents call. Because import_block returns ConsensusError, an invalid seal surfaced as BlockImportError::Other, which the sync layer treats as a transient error: it warns, wipes sync state and restarts, with no DropPeer and no reputation change. A connected peer could therefore serve blocks with garbage proof-of-work indefinitely at no cost, forcing repeated sync restarts that also cancel in-flight requests to honest peers. Move the seal check into the verifier the import queue calls, before check_inherents. An invalid seal now returns Err from verify(), which sc-consensus maps to BlockImportError::VerificationFailed(peer_id, ..), triggering the existing DropPeer(BadPeer(peer, rep::VERIFICATION_FAIL)) path. import_block keeps its own check for locally mined blocks, which bypass the queue verifier. Measured cost of the extra runtime call on the sync path is ~77us per imported block, negligible next to check_inherents and execution. Adds verifier tests (invalid seal rejected, valid seal accepted, wrong-length seal rejected) using a mocked QPoWApi. Addresses report 88219. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* demostrate tip exfiltration on hs account * fix: reject tips from high-security signers The call whitelist cannot see ChargeTransactionPayment's tip, so a compromised key could drain free balance in the same block. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt * refactor: rename payment wrapper to HighSecurityChargeTransactionPayment The wrapper type was shadowing pallet_transaction_payment::ChargeTransactionPayment. Keep the inner IDENTIFIER so signed payloads and metadata stay unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: cap high-security signers at 16 extrinsics per rolling day A compromised key can still pay inclusion fees on whitelisted no-ops. Record each included extrinsic in a 16-slot block-number ring and reject the next until the oldest falls outside DAYS. Update is O(1). Co-authored-by: Cursor <cursoragent@cursor.com> * fix: drop Vesting::claim from the high-security whitelist Claim is permissionless and always pays the stored beneficiary, so a high-security signer does not need it. Leaving it listed was only another no-op fee path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: restrict high-security batch_all to a flat leaf batch Reject empty and nested batches and cap children at MaxPendingPerAccount so a packed wrapper cannot inflate the inclusion fee. A guardian can still cancel a full pending set in one extrinsic. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: reject padded MultiAddress dest on high-security schedule_transfer (#8) A stolen HS key can attach MultiAddress::Raw to schedule_transfer and inflate the length fee (up to ~3.75 UNIT per tx) without a tip. Restrict the HS whitelist to MultiAddress::Id so the padding is rejected before fees are withdrawn. batch_all children use the same check. * fix: cap encoded length of high-security extrinsics The length fee is the last attacker-controlled fee input after the zero-tip and Id-only/flat-batch whitelist gates: it is charged on the full encoded extrinsic pre-dispatch and never refunded, so a stolen key could still pad a future variable-length field and grind free balance out to a colluding block author. Reject high-security extrinsics over MAX_HIGH_SECURITY_EXTRINSIC_LEN (10 KiB) in both validate and prepare, before any fee is withdrawn. * fix: quota ring rejects instead of panicking on a zero-capacity window Extract the ring admission predicate into one shared hs_ring_has_room so mempool validation and inclusion-time recording can never disagree on a window boundary, and make the at-capacity-and-empty case (a misconfigured MaxHighSecurityTxsPerWindow = 0) report no room instead of claiming room and panicking the head eviction inside block execution. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: stop over-charging every extrinsic for the high-security quota write ReversibleTransactionExtension charged 2 reads + 1 write unconditionally with no post-dispatch refund, so all non-high-security traffic paid for a quota ring touch that never happens. Decide the high-security status once in validate, carry it through Val/Pre, gate the quota check and recording on it, and refund the unused ring read+write in post_dispatch_details on the non-HS path (verified end-to-end by the exact-fee assertion in ml_dsa_65). This also collapses the redundant HighSecurityAccounts reads on the validation hot path and drops the ensure_signed origin clones in favour of AsSystemOriginSigner. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop dead tip accessor and unreachable prepare-side tip re-check HighSecurityChargeTransactionPayment::tip() had no caller, and the prepare-side reject_high_security_tip could never fire: dispatch_transaction runs validate immediately before prepare on the same state, and neither the tip nor the signer's high-security status can change between the two. Co-authored-by: Cursor <cursoragent@cursor.com> * test: deduplicate the signed-extrinsic builder and fee-event helpers Hoist the hand-maintained 12-element TxExtension tuple + Dilithium signing into TestCommons::signed_extrinsic and have signed_call / signed_transfer delegate to it (node/src/benchmarking.rs keeps its copy: the node crate cannot depend on runtime test code). Collapse paid_tip / paid_fee_or_zero into one fee_paid event scan, reuse assert_tip_cannot_move_extra_value in the held-pending test, and replace the whitelist-rejection integration tests with check_call unit coverage (adding the missing Vesting::claim unit test). Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: give the high-security batch arity cap its own constant The batch_all leaf cap reused MaxPendingPerAccount, so a future bump of pending-transfer capacity would have silently widened the maximum fee surface of a single high-security extrinsic. MaxHighSecurityBatchLen (16) decouples the two. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: enforce the high-security zero-tip policy in OnChargeTransaction The wrapper extension impersonated the stock ChargeTransactionPayment IDENTIFIER, so a refactor back to the unwrapped extension would compile with byte-identical metadata while silently reopening the tip channel. HighSecurityFungibleAdapter (wrapping FungibleAdapter) now rejects a non-zero tip from a high-security signer in can_withdraw_fee and withdraw_fee — every fee path goes through it, so the policy survives any extension-tuple change. The tuple reverts to the stock extension; wire format and metadata are unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: cap the zero-tip inclusion fee of high-security extrinsics at 1 UNIT The length cap bounds one fee input; weight is the other, and no shape rule on today's whitelist constrains a future call's benchmarked weight. ReversibleTransactionExtension::validate now rejects a high-security extrinsic whose compute_fee(len, info, 0) exceeds MAX_HIGH_SECURITY_INCLUSION_FEE (Custom(5)). The ceiling is ~10x the costliest legitimate call (recover_funds, ~0.098 UNIT) and a headroom test pins 2x margin, so re-benchmarking drift cannot lock accounts out. With the 16/day quota this hard-caps a stolen key's fee grind at 16 UNIT per rolling day regardless of future whitelist changes. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: require guardian acceptance for high-security enrollment set_high_security enrolled immediately, naming any account as guardian without its consent: an attacker could saddle an account with guardian duties, or fill a well-known guardian's 32 bounded GuardianIndex slots with throwaway enrollments so legitimate users could no longer choose it. Enrollment is now a handshake. set_high_security parks an offer in PendingHighSecurityOffers (one per account; a repeat call replaces it, retract_high_security_offer withdraws it). Nothing takes effect — and no GuardianIndex slot is consumed — until the named guardian calls accept_high_security (call indices 8/9). Genesis enrollment stays immediate, being operator-controlled. Co-authored-by: Cursor <cursoragent@cursor.com> * docs+test: recommend and pin multisig guardians for high-security accounts The guardian holds instant, total seizure power (recover_funds sweeps every hold plus the whole free balance to it, no delay, no second approver, immutable relationship), making a single-key guardian a single point of failure for the whole scheme. Document the multisig-guardian deployment as the recommended setup and pin it with an integration test: a 2-of-2 pallet_multisig guardian accepts enrollment, cancels a pending transfer during the delay window, and recovers funds, each dispatched as the multisig via propose/approve/execute. Also drop the stale schedule_asset_transfer references from the pallet README. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop the guardian consent step and the on-chain GuardianIndex GuardianIndex was a bounded (32-slot) guardian -> protected-accounts map that nothing on-chain ever read, and set_high_security filled it without the guardian's consent — a stranger could exhaust a popular guardian's slots with throwaway enrollments so legitimate users could no longer choose it. Remove the index (and MaxGuardianAccounts / TooManyGuardianAccounts) instead of gating it behind an acceptance handshake: with no index there is nothing to fill, and being named guardian needs no consent because it grants only passive powers and carries no liability. set_high_security enrolls immediately again; accept_high_security / retract_high_security_offer and PendingHighSecurityOffers are gone. Discovery of "which accounts do I guard?" moves offchain (Subsquid) via HighSecuritySet events. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: charge the actual high-security storage footprint The extension helpers no longer re-read HighSecurityAccounts after validate, and the payment WeightInfo now includes the tip-policy reads so tipped and HS traffic cannot under-declare database work. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt * fix tests * docs+test: pin that a high-security multisig guardian cannot be quota-locked A single-key HS guardian shares the 16/day quota with its own traffic; an exemption would be farmable. The recommended multisig guardian is immune because the derived address never signs, even when itself enrolled as HS. Co-authored-by: Cursor <cursoragent@cursor.com> * fix tests --------- Co-authored-by: illuzen <illuzen@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: drop self-directed wormhole leaves that record no value Self-cancels and self-scheduled executions were inserting ZK-tree leaves for credits that never moved. Drop from==to at the recorder chokepoint, release rather than transfer_on_hold on owner cancel, and record scheduled execution only when source and dest differ. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt * fix comments --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(multisig): depth-limit inner call decode in propose/execute Opaque BoundedCall bytes were turned into a RuntimeCall via an unbounded RuntimeCall::decode. The outer extrinsic depth limit (MAX_EXTRINSIC_DEPTH) only covers the signed envelope, so a signer could store a canonical, deeply nested call (e.g. chained Utility::batch_all or Recovery::as_recovered) that passed pool validation and then exhausted the runtime stack during block construction, surfacing as a contained Wasm out-of-bounds trap. The resulting RuntimeApiError made the block builder roll back the fee/tip/nonce/weight and authorship treated it as a generic invalid, so authors did repeated unpaid work on rolled-back candidates (Immunefi #88969). Decode with decode_all_with_depth_limit(MAX_MULTISIG_CALL_DEPTH) in both propose and execute. The bound equals MAX_EXTRINSIC_DEPTH (256), which Executive already applies safely to every extrinsic, and all-consuming decode also rejects trailing garbage. Bounding at the decode boundary covers every recursive call gadget, unlike a dispatch-time filter that runs after decode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(multisig): keep inner-call decode non-all-consuming Switching propose/execute to decode_all_with_depth_limit rejected the trailing padding bytes that pallet_multisig's propose_high_security benchmark appends to sweep the call-size component, so the generated runtime-benchmark test failed with InvalidCall and scripts/regenerate_weights.sh could no longer benchmark the pallet across its declared range. The vulnerability fix only requires a recursion-depth bound, not all-consuming input. Use decode_with_depth_limit in both propose and execute to restore the prior decoder's tolerance of trailing bytes (the high-security path deliberately stores padded whitelisted calls) while keeping the depth cap that prevents the stack-exhaustion trap. Verified: cargo test -p pallet-multisig --features runtime-benchmarks (65 passed, incl. benchmarking::benchmarks::bench_propose_high_security and tests::propose_rejects_deeply_nested_call_via_depth_limit). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…ored pallet (#12) Remove the five pallet_balances calls nothing on this chain needs: - force_transfer, force_set_balance: root-only seize/mint tools. There is no sudo, so these were reachable only through a root-track referendum; removing them means governance cannot move or mint balances by extrinsic at all. Issuance still changes only through consensus code (mining rewards, fees, burn). - force_unreserve, force_adjust_total_issuance: root-only, never referenced anywhere in the runtime, node, CLI or apps. - upgrade_accounts: lazy legacy-format migration; a no-op on a chain whose accounts were all created on the new AccountData format. The ensure_upgraded failsafe stays (still used internally) and keeps its tests, now exercised directly. Kept: transfer_allow_death, transfer_keep_alive, transfer_all (recovery sweeps, send-max) and burn (voluntary supply burn). Their call indices are unchanged, so wallets and the cold-signer allowlist are unaffected. Also removed: the AdjustmentDirection type, the TotalIssuanceForced event, the IssuanceDeactivated/DeltaZero errors (last two variants, no index shift), their weights and benchmarks. spec_version 145 -> 146, transaction_version 4 -> 5 (extrinsics removed).
…eposits (#15) Social recovery could not be combined with high-security without permanently freezing the owner's config deposit and a rescuer's RecoveryDeposit. Guardian recover_funds is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: charge Quantus ML-DSA-87 overhead for base extrinsic weight Inherited Substrate Development weights under-accounted signed extrinsics by ~7x after the runtime switched to in-WASM ML-DSA-87 verification. Co-authored-by: Cursor <cursoragent@cursor.com> * style: restore overhead weight license headers and satisfy format gates Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vesting): align non-final claims to 25 QUAN so leaf rounding cannot be griefed Permissionless daily claims were fragmenting grants into 1-QUAN Wormhole leaves, each independently flooring the 4 bps fee. Non-final payouts now quantize to 2500 leaf quanta (an exact 4 bps multiple); the final claim still pays the remainder. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(runtime): drop redundant vesting const asserts The 25 QUAN pin and the 2500×4 tautology added no compile-time safety beyond the existing SCALE_DOWN match and the runtime vesting tests. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(vesting): use saturating ops instead of expect on claim-path math Generic Balance cannot prove remainder ≤ amount or that 2500×quantum fits. saturating_sub/mul are total and exact for these invariants. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vesting): check accrual before the dust-reservation branch in claim_plan Small-remainder schedules (< 2x MinimumPayout) returned ClaimWouldLeaveDust instead of NothingToClaim when nothing claimable had accrued yet. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
#17) * fix: bound Noise handshake frame length before allocation Reject empty or oversized length prefixes so an unauthenticated peer cannot force a 65 KB pre-read per pending connection. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: charge CollectedFees storage on the payment-extension weight The stock transaction-payment benchmark never ran TransactionFeesCollector, so every paid extrinsic omitted one unique-key read and write. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: do Noise PQ work only after the first handshake frame arrives A stalled /noise negotiation no longer forces ML-KEM keygen or an ML-DSA signature; the listener reads the bounded frame first, and both sides defer identity signing until a real peer message is in hand. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: reject wormhole exit bundles that mint nothing A non-dummy zero-output spend is a valid circuit proof but settles no value; refuse it before writing UsedNullifiers so it cannot occupy slots or block weight for free. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: refund unused wormhole batch weight after sparse settlement Unsigned public batches declared the full 742-exit quota and kept it on success, so one real exit could stuff a block. Report actual settlement weight so leftover quota returns to the block. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt * fix: reject valued exit slots with zeroed accounts, review cleanups The no-mint gate only checked amounts, so a segment whose valued slots all have circuit-zeroed exit accounts could settle without minting; mirror the mint-loop condition exactly. Also dedupe the handshake prefix tests, simplify the settlement weight selection, and drop a tautological weight assertion. Co-authored-by: Cursor <cursoragent@cursor.com> * rm pr * fix: charge nullifier writes independently of minted exits A valid zero-output segment writes UsedNullifiers while minting nothing, so the settlement refund undercharged public batches that combined one real mint with many such segments. Also reject listener message-1 frames shorter than the ML-KEM-768 public key before keygen. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vesting): retarget as a same-owner wallet swap, never a payout Retargeting is a lost-key remedy, so paying the outgoing address would burn funds or pay a thief. Drop the claim-plan settlement, keep the accrual on the schedule, and flatten the remaining claim/end arithmetic so the two payout policies share one quantized unpaid helper. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt --------- Co-authored-by: Cursor <cursoragent@cursor.com>
total_balance included reserved funds, and an exit mints free balance without consuming the reserve. Record only spendable genesis endowments so a future reserved genesis cannot become a second, unlocked copy. Co-authored-by: Cursor <cursoragent@cursor.com>
…kets (#23) FRAME's 30s future slack plus 8s retarget bins let one author-supplied timestamp book a difficulty deficit that later +1 catch-up blocks cannot repay. Match Geth's 15s limit and Ethereum's Δt//10 buckets so a max-drift inflate is repayable. Co-authored-by: Cursor <cursoragent@cursor.com>
…#22) * fix(runtime): reserve worst-case wormhole weight on Multisig::execute Execute only carries a proposal id, so the recorder charged nothing for inner transfers and billed the work after the fact. Charge MaxCallSize / min transfer encoding up front, same as recover_funds, and refund unused units after dispatch. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt * fix(runtime): walk stored multisig calls when reserving recorder weight MaxCallSize/36 under-counted a stored batch_all of recover_funds plus transfers, so execute reserved fewer proofs than it recorded and the shortfall was not fee-charged. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt * fix(runtime): price execute proposal walks and skip the prepare re-read A no-transfer stored call reserved zero extension weight while weight() and prepare() each decoded the proposal; failed non-signer executes then refunded that zero. Charge two worst-case walks on every execute and carry the transfer count so prepare does not walk again. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(litep2p): reject inbound multistream-select frames above 1 KiB A two-byte varint could force a 16 KiB resident read buffer before the Noise handshake. Cap the declared length to what negotiation actually needs so an unauthenticated peer cannot pre-commit that allocation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(multisig): reject duplicate signers instead of silently dropping them Silent dedup could hide a repeated account and change the effective threshold. Fail create with DuplicateSigners so the submitted set is exactly what is stored and hashed. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(vesting): never block end_schedule on sub-minimum vested dust A permissionless claim can leave a vested remainder below MinimumPayout, which previously made clawback impossible until maturity. Ending now pays the beneficiary the nearest quantum and returns every leftover planck to the treasury, which does not need a quantized payout. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vesting): refund sub-minimum end payouts to the treasury A nearest-quantum end_schedule payout of one leaf quantum cannot survive the Wormhole volume fee, so a keyless beneficiary would be stranded. Amounts below MinimumPayout now ride to the treasury with the unvested remainder; ending still always succeeds. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(dilithium): bind ML-DSA signatures to FIPS 204 contexts Prevent a signature produced for one purpose (extrinsic vs node identity) from verifying in another, even over the same message and key. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt * docs(runtime): record that spec_version is bumped at release, not in feature PRs Reviewers keep asking feature PRs to increment VERSION; the Release Proposal workflow already does that when is_runtime_upgrade is set. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(p2p): keep empty ML-DSA context so mixed-version Noise handshakes still work Node keys are not account keys; extrinsics already use QUANTUS_EXTRINSIC, so a p2p signature cannot verify on-chain. Co-authored-by: Cursor <cursoragent@cursor.com> * fmt Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#29) A declared length prefix used to BytesMut::resize the payload buffer before any body arrived, committing resident memory on an unauthenticated socket. Grow only as bytes are actually read. Co-authored-by: Cursor <cursoragent@cursor.com>
* runtime: derive all absolute-QUAN prices from a single FEE_SCALE dial * runtime: allow clippy::modulo_one in fee_scaled_coeff while FEE_SCALE is 1/1 * runtime: derive treasury signer seed and test invariants from the FEE_SCALE dial - Planck treasury signers are now seeded with treasury_signer_seed(), computed from the scaled multisig fees/deposit plus the MaxInnerCallWeight inclusion prepay, ED, and scaled headroom - turning the dial can no longer strand treasury bootstrap. Pinned end-to-end by a new test that runs create_multisig and the first propose through Executive::apply_extrinsic on exactly the seeded balance. - pallet-multisig: propose fee formula extracted into pub proposal_fee() and reused by genesis. - tech_collective collateral invariant now uses the runtime's scaled preimage_amount() (made pub) instead of a hardcoded unscaled formula. - RUNTIME_SURFACE.md: ScaledIdentityFee and FEE_SCALE-relative figures.
Sum independently rounded private-segment fees and canonicalize nullifier pool tags so public settlement matches the circuit boundary.
…fails (#31) A failed inner transfer rolled back the hold release while Scheduler dropped the task. Execute with allow_death and keep bookkeeping committed independently of the inner result. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Stacked follow-up for the execute decode-depth issue (not present on either
|
|
Stacked follow-up for execute inner-call walk charging (not on either A no-transfer |
Public's miner protocol stores the last broadcast job and hands it to newly connecting miners. The merge kept that server but paused authoring (stale tip, unreadable clock, offline after grace, shutdown) without clearing the job, so a miner connecting during a pause ground stale work. Clear on the enabled-to-disabled edge only; do not gate on network sync state.
Testnet never exposed recovery to clients, so there are no live |
Index 16 stays vacant. No migration and no client-facing recovery surface: the exercise phase and the removed-pallet section are gone.
|
No recovery migration will be done! Nobody used this feature on testnet, it's not exposed in any client On mainnet, we won't have it at all. No migration needed We updated the docs to no longer refer to recovery |
|
All issues have been addressed, some with new PRs some with merge fixes in here. Making a new review. |
There was a problem hiding this comment.
Reviewer model: GPT Sol
Verdict (advisory): Request changes
I re-reviewed exact head e2d1f1b8ac88d6c0f15774fe29ba418e5ea89cca against base 308ba838d585a8dc8787e18d55ee15e5a4c41d76, including all three commits added since the prior review. The QPoW digest-window validation and authoring-pause miner-job clearing now address those findings. Given the stated deployment facts and the earlier live empty-state check, I accept removal of Recovery without a migration as an operational choice; rechecking that zero-state precondition immediately before the testnet upgrade remains important.
Three Zero blockers remain:
1. [P1] Reserve the outer codec level required by Multisig::execute. MAX_MULTISIG_CALL_DEPTH still equals FRAME's full MAX_EXTRINSIC_DEPTH (pallets/multisig/src/lib.rs:52-68), and propose accepts calls at that boundary (:600-612). The same call must later sit inside execute's Box<RuntimeCall> (:1098-1103), which consumes another decode level, so an accepted boundary proposal cannot be submitted as a valid execute extrinsic. #676 contains the right correction and a boundary regression test, but that change is not in this PR's head. Include the one-level reservation before merging #675.
=> This is not working in either chain or chain-private, therefore we made a separate PR #676
3. [P1] Charge both recursive inner-call walks on every multisig execute. weight() and validate() each call count_transfers (runtime/src/transaction_extensions.rs:657-690), which recursively walks Multisig::execute's caller-controlled inner tree (:476-515). A no-transfer execute reserves zero extension weight, and the current regression test explicitly confirms that a failed non-signer call keeps a zero reservation (:1618-1662) while the pallet reports only its early storage-read work. Repeating broad/deep, no-transfer failing executes therefore consumes two uncharged traversals and returns that capacity to the block. #677 reserves two worst-case walks and retains them on early errors, but it is also absent from #675's head. Bring that fix into the merge.
=> This is not working in either chain or chain-private, therefore we made a separate PR #677
4. [P2] Make the required release exercise command compatible with the removed pallet. docs/RELEASE_PREFLIGHT.md:131-143 now removes recovery from the documented default phases, but live Quantus-Network/quantus-cli main at 96ec0c9b still includes Phase::Recovery in default_set() (source); there is no open matching CLI PR. Consequently the mandated quantus exercise --fail-fast gate still attempts a phase for runtime metadata that #675 removes. Land the coordinated CLI update and make the preflight use that compatible version.
=> CLI and other clients must be updated later!
Validation performed:
git diff --check,cargo +nightly fmt --all -- --check, andtaplo format --checkpassed.SKIP_WASM_BUILD=1 cargo test --locked -p qp-header --lib: 12 passed.SKIP_WASM_BUILD=1 cargo test --locked -p sc-consensus-qpow --lib: 10 passed.SKIP_WASM_BUILD=1 cargo test --locked -p quantus-node: 59 passed.- Both dependency-cooldown checks are green. The exact-head Fast Checks job is still queued with no runner or steps.
|
All these blockers are moot - they're addressed in additional PRs. Ready for final sign-off @illuzen |
Merge chain-private main into chain (re-land of #675 with a merge commit)
Purpose
Merge
Quantus-Network/chain-privatemain(011b2f58) into publicchainmain(308ba838).They split at
803094f7(ci: Miner API version bump to quantus-miner-api-v0.2.1). This branch starts from publicmain(all public commits since the split) and merges privatemainon top. Non-overlapping changes from both sides are kept. Conflicts were resolved per the decisions below; chain-private wins except where this PR explicitly chose otherwise.This is a new unification PR. It does not continue chain-private #41.
What public unique work is kept (no conflict)
#662,#664)#663)#672)#661,#671); they stay revertedConflict resolutions
Nine files conflicted (20 hunks), grouped as originally presented. Choices are recorded in full.
Group A — QPoW digest window
Question: Public (
#665, after#667was reverted by#670) accepts exact 110 bytes, or 111 with exactly oneRuntimeEnvironmentUpdated, at any height, and rejects short encodings (they pad to a colliding hash). Private (#35) uses a length cap only: blocks<= 1_000_000may be 111 bytes (any extra byte); after that, 110. It does not reject short encodings.Choice: Allow both 110 and 111 digests. The block cutoff is OK (keeps testnet history importing). Private API:
max_encoded_digest_size+LEGACY_DIGEST_CUTOFF = 1_000_000. Private verifier tests (invalid PoW / wrong-length seal, report 88219) kept.Group B — mining loop (
node/src/service.rs)Question: Public
#669pauses onis_major_syncing(), clears the stored miner job, and pauses immediately when offline (no 30s grace), so external miners are not handed a stale job they grind for the whole sync. Private#35never reads network sync state (forged peer height, report 88224). Bitcoin-style: stale-tip latch + 30s no-peer grace.--force-authoringbypasses.Choice: Split two different things:
is_major_syncing()pause (forged peer height, report 88224).Group C —
Multisig::executeABI + wormhole weight walkerQuestion: Public
#661, kept by#671:execute(origin, address, id, call). Executor resubmits the call; byte-equal bind to storage. Hardware wallets can see the inner call. Propose also requires canonical encoding (no trailing bytes). Private#11+#22:execute(origin, address, id)only. Decode stored bytes withdecode_with_depth_limit(depth-bounded, not all-consuming). Walker must read storage to price wormhole leaves. Recovery and extra Utility wrappers are already gone in this merge (private-only).Choice: Keep public's call-carrying
execute(clearsigning / self-describing weight / edge-case fix). Stitch:proposestill depth-limits the decode (private Immunefi #88969) and requires canonicaldecoded.encode() == stored bytesso execute's byte-bind cannot strand a proposalcount_transfersrecurses intoexecute { call, .. }(public) and only walksbatch_all(private utility shrink)count_stored_executeremoved; they existed only for the opaque-id ABIGroup D — runtime versions
Question: Public:
spec_version: 145,transaction_version: 4(comment: execute carries the call). Private:spec_version: 147,transaction_version: 6(recovery removal, FIPS context, fees, etc.).Choice: Highest from private: spec 147, transaction 6. Public version numbers dismissed.
Note: this tree's
Multisig::executeencoding is the public call-carrying one. Clients that spoke private's no-call execute need the new argument.transaction_versionis left at 6 as decided.Group E —
docs/RUNTIME_SURFACE.mdextension listQuestion: Public: HS whitelist only; execute carries inner call; still mentions
Recovery::close_recovery. Private: addsHighSecurityTxQuota(16 txs / 24h); execute “walks the stored proposal”; ChargeTransactionPayment note aboutHighSecurityFungibleAdapterzero-tip.Choice: Private text. The execute/walker sentence was updated to “recurses into the resubmitted inner call” so it matches Group C. Quota + tip adapter were already auto-merged.
Group F —
pallets/vesting/src/weights.rs(comment)Question: Public (
#663zk-tree batching):end_schedule/retarget_schedule(= 4) … “flat marginal insert cost”. Private (#19retarget is a same-owner swap, not a payout):end_schedule(= 4) only … “flat circuit-depth insert cost”.Choice: Private comment / retarget-has-no-leaf. Flag: auto-merged public zk-tree made
INSERT_LEAF_*the cheap marginal price. Privatepayout_weightthen undercharges the benchmarked base (payout_weight_never_undercharges_the_benchmarked_basefailed: 1.275s vs 1.305s ref_time). A clamp to the measured base is applied so we do not ship an undercharge; regenerate vesting benchmarks as follow-up. Publicpayout_weight_unclampedhelper is not carried.Group G —
pallets/wormhole/src/weights.rs(comment)Question: Public: one leaf per exit; marginal insert; root batched in
on_initialize. Private: exit leaf plus miner volume-fee leaf (and aggregator rebate on public batch); priced atCIRCUIT_MAX_TREE_DEPTH.Choice: Private comment and extra fee-leaf accounting. Flag: with public batched
INSERT_LEAF_*, the per-exit tail is no longer ~10× the one-exit settled weight (ZK verify dominates). Two tests that requiredsettled(1) * 10 < declaredwere relaxed tosettled(1) < declared; they still prove unused-exit refunds.Stitches required after the picks
External miner protocol is public (nothing was deferred)
The miner wire protocol and the node’s session with an external miner are the public ones. After the split, private never touched
miner-api/ornode/src/miner_server.rs; those files in this merge are public#662/#664/#669. There is no private miner-API patch to land later.What that means in this tree:
quantus-miner/2, authenticatedReady { token }, TLS cert + SHA-256 fingerprint pin, 1 KiB frame limit (miner-apiv0.3.0).MinerServer::start(MinerServerConfig)with auth-token path and TLS dir. If--miner-listen-portis set and the server cannot start, the node fail-closes (no silent fallback to local mining).handle_external_mining— snapshotworker_handle.version()before broadcasting, treat a version change as supersession, do not blame the miner for a seal that arrived after a template rebuild.clear_current_jobexists on the public server so a connecting miner is not handed a stored job while authoring is paused.mining_loopstill decides whether to mine (tip freshness + 30s offline grace, neveris_major_syncing()). That is Group B, not the miner API.Operators must upgrade external miners and provision token + cert pin files before deploying this.
Other stitches
proposebenchmark no longer pads trailing bytes (canonical-encode check from C would reject them).RUNTIME_SURFACE.mdexecute walker wording aligned with C.Follow-up (not in this PR)
Verification (local)
cargo +nightly fmt --allSKIP_WASM_BUILD=1 cargo test --locked -p qp-header --lib— 6 passedSKIP_WASM_BUILD=1 cargo test --locked -p sc-consensus-qpow --lib— 9 passedSKIP_WASM_BUILD=1 cargo test --locked -p pallet-multisig --lib— 60 passedSKIP_WASM_BUILD=1 cargo test --locked -p pallet-vesting --lib payout_weight_never_undercharges— passed after clampSKIP_WASM_BUILD=1 cargo test --locked -p pallet-wormhole --lib— 85 passed, 3 ignoredSKIP_WASM_BUILD=1 cargo test --locked -p quantus-runtime --lib— 68 passedSKIP_WASM_BUILD=1 cargo test --locked -p quantus-runtime --test mod --features fast-governance— 56 passed, 1 ignoredSKIP_WASM_BUILD=1 cargo test --locked -p quantus-node— 59 passed (includes miner auth/TLS)Operational note
Same as the miner-protocol stitch above: this is public
quantus-miner/2with auth + TLS pinning. External miners that still speak the pre-#662protocol will not connect.