Skip to content

multi: asset boarding disclosure and boarded funding (M5 client) - #1108

Merged
darioAnongba merged 38 commits into
darioAnongba/asset-tree-m4-clientfrom
darioAnongba/asset-tree-m5-client
Aug 11, 2026
Merged

multi: asset boarding disclosure and boarded funding (M5 client)#1108
darioAnongba merged 38 commits into
darioAnongba/asset-tree-m4-clientfrom
darioAnongba/asset-tree-m5-client

Conversation

@darioAnongba

@darioAnongba darioAnongba commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

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 TestRoundAssetBoarding in lumos.

Boarding

  • BoardingRequest gains 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.ComposedBoardingScript recomputes 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 / ComposedBoardingAuthSpend give 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.NewBoardedProofVerifier verifies 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.
  • A caller-provided batch funding witness now marks the funding input CallerSigned: a boarded funding input is a boarding input at the Bitcoin level, signed by its owner through the round's boarding-signature flow.
  • Onboarding's asset output is keyed to a digest-scoped OP_TRUE script rather than the local tapd wallet. Keying it to tapd made the output unspendable by its actual owner, and would have had tapd report a balance it cannot spend — it knows nothing of the Ark policy at the Bitcoin layer.

Owning and spending a leaf

  • ClientVTXO carries the leaf's asset identity plus the sealed package the operator hands over on ClientBatchInfo.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; loadCreatedPackage checks the VTXO row before the OOR artifact store, and the existing ResolveCreatedAssetProofSource rebuilds the compact proof path and OP_TRUE witness from it unchanged.
  • Persisting the root matters beyond convenience: coin selection skips a VTXO only when it carries an asset commitment root, so a leaf without one is spendable as plain sats — destroying the assets riding on it. TestBuildClientVTXOsMarksAssetLeaves is 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, so Cosigners[0] stopped being the tree-signing key. This only bites with two or more leaves, which is why M4 never saw it. Fixed with sortableCopy plus a regression test.
  • The tree materializer no longer clobbers a single-leaf tree's subtree total with the root's unauthenticated zero amount.

Removed: direct on-chain asset VTXO registration, along with its RPC surface, state enum and --wait polling. 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.

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.
@darioAnongba
darioAnongba force-pushed the darioAnongba/asset-tree-m5-client branch from 00e775e to 6f529a3 Compare August 7, 2026 11:30
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.
@darioAnongba
darioAnongba force-pushed the darioAnongba/asset-tree-m5-client branch from 31e015a to 7aa4fcc Compare August 8, 2026 17:50
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.
@darioAnongba
darioAnongba force-pushed the darioAnongba/asset-tree-m5-client branch from 9290abd to 41c5e4d Compare August 8, 2026 18:42
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.
@darioAnongba darioAnongba self-assigned this Aug 10, 2026
@darioAnongba
darioAnongba marked this pull request as ready for review August 11, 2026 15:59
@darioAnongba
darioAnongba merged commit 1aa2174 into darioAnongba/taproot-assets-oor-runtime Aug 11, 2026
1 check passed
@darioAnongba
darioAnongba deleted the darioAnongba/asset-tree-m5-client branch August 11, 2026 16:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines 168 to 170
[]byte(nil), result.TaprootAssetRoot[:]...,
),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread round/transitions.go
Comment on lines +4452 to +4457
sealed := assetLeafPackages[*outpoint]
if len(sealed) == 0 {
return nil, fmt.Errorf("asset leaf %s has no "+
"sealed package", outpoint)
}
vtxo.TaprootAssetSealedPackage = sealed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant