fix(mirror): sign mirror spends under the Chia L1 genesis, not the DIG L2 one - #448
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
…G L2 one A mirror coin is an ordinary Chia L1 CAT, so the consensus that validates its spend appends Chia mainnet's genesis challenge to every AGG_SIG_ME message. open_signer opened the operator wallet with dig_constants::DIG_MAINNET's genesis -- the DIG L2 anchor -- so every mirror create and reclaim committed to a message Chia does not check, and the mempool answered BAD_AGGREGATE_SIGNATURE deterministically from every peer. dig-constants is the L2 chain's constants crate and has no business in an L1 CAT spend. That sentence lives on the new mirror_agg_sig_data(), which gives the choice a name a test can assert on. Closes #447 Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
… signed under Co-Authored-By: Claude <noreply@anthropic.com>
…ass guard Co-Authored-By: Claude <noreply@anthropic.com>
loop-security — IN PROGRESS, NOT THE VERDICTAuditing head Item 1 of 6 — CONFIRMED CORRECT: the constant is the real Chia L1 domainRead from the vendored crate source, not assumed:
So One precision note, NOT a defect and not gating: the strictly-correct field name for an Items 2-6 (derivation unchanged, peer network id untouched, single chia line in the lock, test non-circularity, |
loop-security — IN PROGRESS, NOT THE VERDICT (2 of 3)Head Item 2 — CONFIRMED: NO address and NO key derivation changedTraced
Both address-bearing fields come from So the value reaches the Item 3 — CONFIRMED: the DIG peer network id was left aloneAt the head SHA, Corroboration worth recording: Item 6 — CONFIRMED:
|
loop-security: PASSAudited head: Nothing CRITICAL and nothing HIGH. NO address and NO key derivation changed -- the operator's funds stay at the same derived address, and I traced that mechanically rather than taking the claim (evidence in item 2). Method: grep + direct read of git objects at the head SHA ( One correction I had to make mid-audit, recorded because it nearly produced a wrong finding: my first call-site grep ran against the shared checkout's working tree, which sits at pre-fix The six checks1. The constant is the real Chia L1 domain -- CORRECT
2. The value reaches ONLY the AGG_SIG_ME domain -- CONFIRMED, no address moved
One level down, So 3. The DIG peer network id was left alone -- CONFIRMED
Stronger than "the diff did not touch it": Two corroborations worth recording:
4. One chia line, verified from the resolved lock -- CONFIRMEDThe strongest available evidence, and it is decisive: the entire
The workspace does carry older chia lines (0.26 / 0.30 / 0.34) in other subtrees, but that is pre-existing, is not reachable from either crate on this path, and is the condition section 2.4b explicitly notes 5. The test is genuinely non-circular -- CONFIRMEDThis was the check most worth doing, and it holds.
So the signature commits to the message built under Three things that could have made it vacuous, each checked:
The second test ( 6.
|
DRAFT — do not merge. No gate round has run.
Closes #447. Parent epic: https://github.com/DIG-Network/dig_ecosystem/issues/3166
The defect
mirror::lifecycle::open_signeropened the operator wallet withdig_constants::DIG_MAINNET.genesis_challenge()—0af981862a4df51f51ec59c312315d959931d917c375730b89b9e2b0854d1abf, the DIG L2 genesis anchor.A mirror coin is an ordinary Chia L1 CAT, so the consensus that validates its spend appends
Chia mainnet's genesis (
ccd5bb71...) to everyAGG_SIG_MEmessage. The wallet therefore produceda valid signature over a message the network does not check, and Chia's mempool answered
BAD_AGGREGATE_SIGNATURE->TransactionAck.status = 3->ack: FAILED, deterministically, fromevery peer. Nothing failed locally: the bundle built, signed and broadcast.
Regression from
5df3e34(#419).v0.170.0/v0.172.0clean;v0.179.0/v0.183.0affected.The fix
A named selector,
mirror::lifecycle::mirror_agg_sig_data(), returningchia_sdk_types::MAINNET_CONSTANTS.genesis_challenge— the cratechia_wallet_sdk::typesre-exports, named directly so this crate does not pull the whole SDK for one constant. It was
already a dev-dependency at 0.36.0, so no second chia line enters the tree.
The reason lives in the function's doc comment:
dig-constantsis the DIG L2 chain's constantscrate and has no business in an L1 CAT spend. That is what stops it being re-introduced, and it is
now also normative in
SPEC.md§25.2.Blast radius
gitnexus'sdig-nodeindex predatesopen_signer(new in #419) and returnedTarget not found,so this was scoped by grep + direct read — §2.0 permits the fallback and requires saying so.
open_signerhas exactly one production caller,server.rs:2691(bring-up), plus two in-filetests. Its signature is unchanged, so no caller adapts.
WalletSigner::new(agg_sig_data)and thence into theAGG_SIG_MEmessage domain.
owner_puzzle_hashandsynthetic_keyderive from the phrase and are unaffectedby it (
operator_wallet.rs:82-88) — no address or derivation changes.dig_constants::DIG_MAINNET.genesis_challenge()indig-node-serviceor
dig-wallet. It remains in use for the DIG peer network id (peer.rs:509/542), which iscorrect there and untouched.
dig_wallet::sage::spend::required_bls_signatures, an additive extraction over the existingrequired_signatures, with no callers changed.Risk read: LOW for the code radius, HIGH for the behaviour it restores — this turns an
unspendable money path back on, which is the point.
Tests — red proven by reverting the production line
crates/dig-node-service/tests/mirror_l1_genesis.rs, 3 tests. The load-bearing one is deliberatelynot an assertion about a constant: it signs a real reclaim with the wallet the production
selector builds, then verifies that signature against the message Chia's own consensus requires,
with the L1 domain stated independently in the test file. A signature made under the L2 domain
cannot verify against the L1 message.
Green at
275e767—running 3 tests/3 passed; 0 failed.Production line reverted to the L2 constant, same run,
0 passed; 3 failed, each on its ownassertion:
a_reclaim_signed_by_the_production_wallet_verifies_under_the_chia_l1_domainaggregate_verifyassertionthe_required_message_ends_in_the_chia_mainnet_genesis_challengeleft: 0af98186... right: ccd5bb71...the_mirror_signing_domain_is_never_a_dig_constants_genesisassertion left != right failedRestored, tree clean.
Proven on mainnet
Mirror coin
31e9077265a6254c32d3857a5cff3561c3b57d28e3bb7789b24173be6c89f14d, 1010 $DIG baseunits, unspent since block 9,224,641, was reclaimed by a 0.187.0 binary from this branch.
The audit journal shows the exact change in the network's answer:
failed / broadcast— "the network did not admit the transaction to its mempool (ack: FAILED)"submittedVerified on
api.coinset.org, not from the node:spent: true,spent_block_index: 92252540xe0534fd8f369cf8a32646d131cf8593fb7e97f7e9aa1ec0ea901045fb05deffb— the operator's own $DIG address — confirmed in block 9225254, unspentVersion
Workspace
0.185.0->0.187.0(Cargo.toml+Cargo.lock). Patch would understate it: thisrestores a capability that could not execute at all.
Follow-up found, not fixed here
After a successful broadcast the journal records
unresolved: "the producer ended without recording an outcome"rather than a confirmation — the spend landed on chain while its own auditentry never says so. Off the critical path per §2.6; filed rather than fixed.