|
| 1 | +TestTaprootAssetsDaemon/tranche00/83-of-98/anchor_multiple_virtual_transactions was failing inside ValidateAnchorInputs |
| 2 | +with “anchor input script mismatch”. The mismatch appeared whenever we executed the PublishAndLogTransfer (pre‑anchored) |
| 3 | +path: the RPC would validate the user-supplied PSBT/vpkts, |
| 4 | +but once ChainPorter picked up the resulting parcel it no longer had the full tap trees needed to recreate each anchor |
| 5 | +input, so the final pre-broadcast check failed. |
| 6 | + |
| 7 | +Root cause |
| 8 | + |
| 9 | +ValidateAnchorInputs needs all assets that were committed in the original anchor input: |
| 10 | + |
| 11 | +1. active + passive assets (present in the virtual packets), and |
| 12 | +2. “purged assets” (tombstones/burns) that were part of the commitment but are not recreated. |
| 13 | + |
| 14 | +When ChainPorter orchestrates a send from scratch it keeps the original InputCommitments in memory, so in |
| 15 | +SendStateVerifyPreBroadcast we can call tapsend.ExtractUnSpendable on those commitments and hand the pruned leaves to |
| 16 | +ValidateAnchorInputs. However, in the pre‑anchored RPC flow we never stored |
| 17 | +those commitments—InputCommitments is empty—and although rpcServer.validateInputAssets fetched and used the purged |
| 18 | +leaves to check the user’s PSBT, it didn’t persist them anywhere. By the time ChainPorter ran, the only data left were |
| 19 | +the active/passive assets; the tombstones/burns were gone, so the |
| 20 | +reconstructed tap tree didn’t match the on-chain script. |
| 21 | + |
| 22 | +What changed |
| 23 | + |
| 24 | +1. rpcServer.validateInputAssets now returns the pruned assets it discovers while validating a PSBT’s inputs. The helper |
| 25 | + still performs all prior checks (version validation, local key derivation, commitment lookups, supply conservation), |
| 26 | + but it also hands the tombstones/burns back to the caller. |
| 27 | +2. PublishAndLogTransfer captures that map and passes it down to ChainPorter by storing it in the PreAnchoredParcel. |
| 28 | +3. sendPackage and ChainPorter gain a PrunedAssets field. When we reach SendStateVerifyPreBroadcast, we merge any |
| 29 | + pre-supplied pruned leaves with the ones we can still derive from InputCommitments and feed the combined set into |
| 30 | + ValidateAnchorInputs. |
| 31 | +4. Existing internal callers of NewPreAnchoredParcel (aux funding controller, aux closer) pass nil because they still |
| 32 | + have full commitments in memory; nothing changes for those flows. |
| 33 | + |
| 34 | +This ensures that every code path which validated a PSBT against the full tap tree can supply the same tombstones/burns |
| 35 | +later, so the final pre-broadcast check reconstructs the exact script that is committed on chain. |
| 36 | + |
| 37 | +Why not just populate InputCommitments? |
| 38 | + |
| 39 | +For pre-anchored flows we often cannot build the full InputCommitments map: |
| 40 | + |
| 41 | +- Inputs might belong to another party; we can’t fetch or persist their entire tap commitments. |
| 42 | +- Even for local inputs the full commitment can be large, and we’d only be storing it to re-derive a small subset (the |
| 43 | + unspendable leaves). That’s heavyweight and redundant. |
| 44 | + |
| 45 | +Passing just the pruned leaves is the minimal data we need to satisfy ValidateAnchorInputs, and it works even when the |
| 46 | +full commitments aren’t available. |
| 47 | + |
| 48 | +Is rpcServer.validateInputAssets redundant now? |
| 49 | + |
| 50 | +No. It serves as the early validation barrier at the RPC boundary: |
| 51 | + |
| 52 | +- It decorates the PSBT with local derivation info (so later signing works). |
| 53 | +- It fetches whatever commitments the node knows about to enforce supply conservation and collect pruned leaves. |
| 54 | +- It ensures malformed PSBTs are rejected before we ask the wallet to fund/sign or modify any state. |
| 55 | + |
| 56 | +ChainPorter’s validateReadyForPublish is the final gate after coin selection and passive re-anchoring. Both stages call |
| 57 | +into ValidateAnchorInputs/Outputs, but at different points in the lifecycle and with different responsibilities. |
| 58 | +Removing either would either expose us to malformed user input (if |
| 59 | +we removed the RPC check) or allow inconsistencies to slip through right before broadcast (if we removed the ChainPorter |
| 60 | +check). The new change simply lets the early-stage information flow to the late stage so both checks see the same |
| 61 | +complete data. |
| 62 | + |
| 63 | +Summary |
| 64 | + |
| 65 | +- Returning pruned assets from the RPC-layer validation and carrying them through the parcel keeps tombstones/burns |
| 66 | + alive for the final validation step. |
| 67 | +- ChainPorter still gathers unspendables from InputCommitments when it has them; the new field only matters for |
| 68 | + pre‑anchored workflows that previously had no way to reproduce the missing leaves. |
| 69 | +- validateInputAssets remains necessary as the RPC boundary guard; the additional return value just makes its work |
| 70 | + reusable later. |
0 commit comments