multi: asset boarding disclosure and boarded funding (M5 client) - #1108
Conversation
A boarding request can name the Taproot Asset its output carries: the asset reference, amount, OP_TRUE digest, and confirmed proof file. The identity fields are bound into the join-auth digest; the proof file is chain-authenticated and stays outside the signature. A caller-provided batch funding witness now marks the funding input caller-signed, since a boarded funding input is a boarding input at the Bitcoin level, signed by its owner through the round's boarding-signature flow.
The composed-script recompute authenticates it, and it is the tapscript sibling the operator needs to assemble the collaborative spend's control block on a composed boarding output.
The operator authenticates a boarded output without decoding proof internals: the boarding policy's canonical root branches with the disclosed commitment leaf hash, and byte equality with the on-chain script pins the disclosure.
The round's commitment transition spends the boarded asset with its OP_TRUE script-path data, which only the creating wallet holds. The stack carries no authority — the asset script key is anyone-can-spend by design, custody being the Bitcoin-level boarding policy — so it stays outside the auth digest; a wrong stack fails the sealed commit.
The commitment leaf hash and witness stack were populated on the type but never serialized onto the wire request.
A boarded UTXO lives in the client's wallet, so the inventory membership check cannot apply; the tip is pinned to the boarding claim and completeness rests on the committer's passive-asset rejection.
The wallet's address watcher cannot recognize composed outputs, so a caller supplies the full intent material — boarding key, confirmation, and the asset disclosure — and the actor assembles it into the next round's intent package. A waved test hook exposes it for integration harnesses.
The commitment transition accepts one funding source per boarded input: per-source amounts, proofs, witnesses, and signing plans, with the request total pinned to the sources' sum. The tree root source becomes a V1 compact path whose additional confirmed bases carry the co-inputs' lineages, bound to the transition by the path verifier. Requires tap-sdk multi-base proof paths (#174).
The boarding disclosure's leaf hash is fully determined by the anchor's commitment root and amount for a single-asset anchor, so the onboarding result can produce it without new persistence.
The digest and the OP_TRUE witness stack tapd returned at commit are what a round's boarding disclosure needs; carrying them on the result saves re-deriving them from the sealed package.
Replaying the idempotent onboarding by request ID yields everything the boarding disclosure needs; the caller adds the chain confirmation and the exported proof file.
Cross-tapd transfers need a proof courier address other containers can reach; the Docker network name is that address.
00e775e to
6f529a3
Compare
The onboarding output branches the Taproot Asset root directly with the policy root, so that root already is the sibling the operator needs. Reconstructing a leaf hash from the root and amount assumed a different convention and produced a script that did not match the chain.
A round's transition is built by the operator, which cannot sign an asset script key owned by the boarder's tapd wallet. Boarding mode keys the output to a digest-scoped OP_TRUE script instead; custody is unchanged because spending still requires the composed Bitcoin output's collaborative leaf. Boarding outputs also skip the direct-VTXO registration: the round that consumes the output is what admits it.
The wallet never derived the composed output, so the join-auth proof-of-funds script and the script-spend control block have to be rebuilt from the disclosure. Owner and operator now reach that derivation through one arkscript helper so the two cannot drift.
A round-boarding output is spent as a boarding input, so it must carry the operator's boarding exit delay; the shorter VTXO delay leaves too little safety margin and admission rejects it.
Join-round authorization spends the timeout leaf, whose control block on a composed output needs the disclosed commitment leaf hash as an extra sibling. Without it the witness rebuilds the policy-only key and the operator rejects the proof.
The bare message gave no way to tell a wrong cosigner set from a wrong tweak.
MuSig2 sorts the slice it is handed in place. Callers identify a participant by position — the asset tree takes its operator key from cosigner zero — so aggregating for the batch output silently moved a leaf owner into that slot and every tree node was then built for the wrong signer set.
tapd chooses which of a multi-source transition's inputs the proof continues, so assuming the first source left the confirmed base and the recorded step describing a different input than the one tapd named.
A round checkpoints each boarding intent against the wallet's own records, and the wallet never derived this composed address, so the checkpoint failed on a foreign key the moment the client had signed.
It existed only for the operator-derived asset tree fallback, which is gone: every asset leaf belongs to the client that requested it and is cosigned through the round's own ceremony.
Direct registration predates asset rounds: it admitted a confirmed tapd anchor straight into the operator's VTXO set because there was no tree to board it into. There is one now, so onboarding exists only to create a composed boarding output for a round to consume. That removes the RegisterTaprootAssetVTXO RPC and its whole client side — the registrar, the operator-admission state machine, and the pending/ready reporting the CLI polled on. The asset output is now always OP_TRUE-keyed, since a round's operator is what spends it.
The poll interval, the onboard-call alias, an outpoint helper, and a stub error field all existed only for the deleted admission flow.
Only the version header differs; the earlier local generation matched byte for byte otherwise.
Coin selection skips a VTXO only when it carries an asset commitment root. A round asset leaf had none — the client validated its own request against the tree and then dropped the ref and amount — so the leaf was selectable as ordinary sats and spending its carrier destroyed the asset it held. The operator's own row was asset-null too, so nothing stopped it on either side. The operator now discloses each leaf's commitment root alongside the per-node material the tree already carried, and the owner rebuilds the composed script from it and requires the leaf output to match. That fixes a second bug in passing: the stored pkScript was the plain policy script, never the composed one the leaf actually pays to.
Spending an asset leaf out of round needs the compact proof path and the OP_TRUE witness behind it, and the owner has no source for either: the operator holds every tree node's sealed package. It now ships each client its own leaves' packages with the round's batch info — scoped to that client, so no other node's package leaves the operator — and the owner persists it beside the leaf. The existing proof-less OOR path resolves a spend from exactly such a package, so this is the material it was missing rather than a new mechanism.
31e015a to
7aa4fcc
Compare
A node merging several boarded asset inputs failed roughly half its commits because the SDK's commit verifier folded alternate-leaf order into a byte comparison. Repin once tap-sdk #175 merges.
9290abd to
41c5e4d
Compare
A refresh round carries no asset transition, so forfeiting an asset leaf into one destroys the asset commitment while preserving only its carrier sats. The expiry ladder now leaves the coin live; the critical-expiry escalation still broadcasts its tree.
The sweep spends the target to a plain wallet output, which would destroy the asset commitment. The unroll still broadcasts the tree, which is the exit itself: the composed output lands on chain under the owner's exit leaf. Claiming the assets takes an asset-aware spend.
A composed boarding output embeds an asset commitment branch, so the plain timeout sweep would destroy the boarded assets. Detect it by recompiling the plain policy script from the intent's own keys and comparing with the stored address.
1aa2174
into
darioAnongba/taproot-assets-oor-runtime
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5471a4f0f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| -- that created it. Spending it out of round needs that package to rebuild | ||
| -- the compact proof path and the OP_TRUE witness, and the owner has no | ||
| -- other source for either: the operator holds every tree node's package. | ||
| ALTER TABLE vtxos ADD COLUMN taproot_asset_sealed_package BLOB; |
There was a problem hiding this comment.
Append a migration for the sealed-package column
Databases created before this change can already be at migration version 19, so modifying the previously applied 000018 migration does not add this column during upgrade. The regenerated VTXO queries now select and write taproot_asset_sealed_package, causing existing installations to fail with a missing-column error when they access VTXOs. Add this as a new migration after the existing baseline and bump LatestMigrationVersion instead.
AGENTS.md reference: db/AGENTS.md:L144-L150
Useful? React with 👍 / 👎.
| []byte(nil), result.TaprootAssetRoot[:]..., | ||
| ), | ||
| } |
There was a problem hiding this comment.
Expose a production path that submits the boarding disclosure
When a user invokes the shipped taproot-assets onboard command, this RPC now returns immediately after creating the composed output, but no public RPC or CLI subsequently accepts its confirmation, proof, and witness to register it with the round actor. The only callers capable of constructing and sending RegisterAssetBoardingRequest are the in-process methods in server_round_testhook.go, while the wallet watcher explicitly cannot recognize these composed outputs. Consequently, the normal daemon/CLI flow creates the asset-bearing output but can never board it into a round.
Useful? React with 👍 / 👎.
| sealed := assetLeafPackages[*outpoint] | ||
| if len(sealed) == 0 { | ||
| return nil, fmt.Errorf("asset leaf %s has no "+ | ||
| "sealed package", outpoint) | ||
| } | ||
| vtxo.TaprootAssetSealedPackage = sealed |
There was a problem hiding this comment.
Validate sealed leaf packages before signing the round
If the operator supplies any non-empty but malformed package, or a valid package for a different leaf, this check accepts it and the client completes the signing ceremony and persists it as the leaf's only proof source. Binding and decoding happen only later in tapassets.ResolveCreatedAssetProofSource when an OOR spend is prepared, at which point the confirmed asset VTXO cannot be spent because there is no alternative source for its proof path or OP_TRUE witness. Validate the package against this outpoint, carrier value, asset identity, amount, and commitment root before accepting/signing the round.
Useful? React with 👍 / 👎.
Client half of Milestone 5 (asset round trees), stacked on #1107. Operator counterpart: lumos#762.
Together they carry the headline path end to end — mint → board → asset VTXO at a tree leaf → OOR — verified by
TestRoundAssetBoardingin lumos.Boarding
BoardingRequestgains the asset boarding disclosure: ref, amount, OP_TRUE digest, confirmed proof file, commitment leaf hash, and the OP_TRUE witness stack. Identity fields are bound into the join-auth TLV; the proof is chain-authenticated and the witness carries no authority (the asset script key is anyone-can-spend by design — custody is the Bitcoin-level boarding policy), so both stay outside the signature.tapassets.ComposedBoardingScriptrecomputes a boarded output's script from hashes alone (policy root branched with the disclosed commitment leaf hash), unit-tested against independent txscript derivation — the operator's trustless authentication primitive.arkscript.ComposedBoardingAddress/ComposedBoardingAuthSpendgive the join auth a pkScript and spend path for the composed output: the auth uses the timeout leaf, the commitment tx the collab leaf, and both need the extra sibling.tapassets.NewBoardedProofVerifierverifies a boarded proof chain and pins its tip to the boarding claim; anchor-inventory membership deliberately does not apply, and passive completeness rests on the committer's PassiveReject policy failing closed at commit.Owning and spending a leaf
ClientVTXOcarries the leaf's asset identity plus the sealed package the operator hands over onClientBatchInfo.asset_leaf_packages. A round leaf is an unconfirmed tree node with no exportable proof, so that package is the only source for its lineage;loadCreatedPackagechecks the VTXO row before the OOR artifact store, and the existingResolveCreatedAssetProofSourcerebuilds the compact proof path and OP_TRUE witness from it unchanged.TestBuildClientVTXOsMarksAssetLeavesis the regression test, verified to fail without the fix.Two bugs the multi-boarder round exposed
musig2.AggregateKeys(keys, true)sorts the caller's slice in place, soCosigners[0]stopped being the tree-signing key. This only bites with two or more leaves, which is why M4 never saw it. Fixed withsortableCopyplus a regression test.Removed: direct on-chain asset VTXO registration, along with its RPC surface, state enum and
--waitpolling. It predated asset rounds and was a misunderstanding of how boarding works — Bitcoin has no such path either.Depends on tap-sdk#175; the pin needs updating once that merges.