Skip to content

multi: Taproot Assets Ark integration (PoC) - #1062

Draft
darioAnongba wants to merge 119 commits into
mainfrom
darioAnongba/taproot-assets-oor-runtime
Draft

multi: Taproot Assets Ark integration (PoC)#1062
darioAnongba wants to merge 119 commits into
mainfrom
darioAnongba/taproot-assets-oor-runtime

Conversation

@darioAnongba

@darioAnongba darioAnongba commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Client half of the Taproot Assets Ark integration, consolidated and rebased onto main. Operator counterpart: lumos#731; tracking epic: lumos#772; design: https://taproot-assets-ark.lightning.wiki/.

This branch now carries the whole client side of the proof of concept: the previous stack (#1084, #1091, #1107, #1108, #1128, #1129) was merged into it commit by commit and the result rebased onto current main. The commits stay atomic; the branch will be split into focused, reviewable PRs before anything merges. Treat this PR as the integration point for testing, not as a review unit.

What the branch contains, in stack order:

  • Asset OOR transfers. Mixed asset and Bitcoin out-of-round sends built on tap-sdk custom anchors: proof-selected inputs, sealed transition packages, recipient routing by semantic receive script, and durable resume across restarts.
  • Composed-output primitives (M1). An asset VTXO is a taproot output committing to the Ark policy tree and the Taproot Asset tree as siblings; every Ark script works unchanged at the cost of one extra control-block sibling.
  • Round wire format and codecs (M2). Flow versioning, asset tree codecs, and the disclosure material the operator authenticates from hashes alone.
  • Client-verified asset trees (M4). Clients request asset VTXOs, verify the operator's asset transition against their own tapd, and cosign only trees they have verified.
  • Asset boarding and burn-safety guards (M5). Boarding through the round with multi-user batching, plus guards that keep every plain-Bitcoin flow (coin selection, auto-refresh, sweeps) away from asset-bearing outputs.
  • Asset leaf refresh (M7 slice). A refresh forfeits an asset leaf and asks the same units back as a fixed-amount request; the join-auth proof composes the asset root.
  • Unilateral exit and claim (M6). An unroll materializes the pre-signed tree; the claim completes the leaf's sealed lineage into a confirmed proof file and spends the composed output into a fresh tapd-owned anchor.

Rebase notes, for whoever reviews the eventual split: the OOR durable-message TLV types moved to 8 and 10 (main took 7 for the admission deadline), the asset migrations renumbered to 000017 through 000020 behind main's 000016, the taproot_asset proto field moved to 7 behind main's admission fields, and required-input selection now coexists with main's exact-outpoint selection in the VTXO manager. The test harness now defaults tapd to the released taproot-assets v0.8.1 image so the lumos asset integration tests run unconditionally; they stay red until a tapd release carries the transition-proof v1 support.

Verified after the rebase: full unit suite, plus the four asset integration tests (TestRoundAssetBoarding, TestRoundAssetBatchOutput, TestRoundAssetRefresh, TestRoundAssetExit) and the core Bitcoin boarding and unroll itests against the rebased operator branch.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@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: 1aa2174bbb

ℹ️ 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".

Comment thread lib/tree/tree.go
Comment on lines +450 to +453
// The composed output must at least be a taproot output; its key is
// re-derived and proven at spend time.
if !txscript.IsPayToTaproot(vtxoOutput.PkScript) {
return nil, fmt.Errorf("asset VTXO output is not P2TR")

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 Bind the asset leaf script before signing the round

When an operator returns an asset-aware round, this validation accepts any P2TR output and compares the requested asset identity and amount only with the operator-supplied AssetContext. The client can then sign the tree and release its boarding input signature; the exact composed-script check in buildClientVTXOs happens only after BoardingConfirmed, when the commitment is already on chain. A compromised or misconfigured operator could therefore place the purported asset leaf under a key the client cannot spend and irreversibly consume the boarding input. Validate the sealed leaf package/commitment root and require the resulting composed script to equal the leaf output before signing.

Useful? React with 👍 / 👎.

Comment thread waved/config.go
@@ -47,6 +47,10 @@ const (
// lnd instance.
DefaultLndHost = "localhost:10009"

// DefaultTapdHost is the default address of the local tapd instance
// used by the optional Taproot Asset OOR PoC.
DefaultTapdHost = "localhost:10029"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Choose a tapd default distinct from waved's RPC port

With taprootassets.enabled=true and no host override, the tap-sdk client is configured for localhost:10029, which is also DefaultRPCHost for waved in this file. Consequently the adapter connects back to waved's own gRPC server rather than tapd, and every tapd RPC fails as an unimplemented service. Use a non-conflicting tapd default or require an explicit host when enabling the integration.

Useful? React with 👍 / 👎.

@darioAnongba
darioAnongba force-pushed the darioAnongba/taproot-assets-oor-runtime branch from 1aa2174 to 0dfe1af Compare August 11, 2026 18:04
@darioAnongba darioAnongba changed the title multi: prototype Taproot Asset OOR transfers multi: Taproot Assets Ark integration (client, consolidated) Aug 11, 2026
@darioAnongba darioAnongba changed the title multi: Taproot Assets Ark integration (client, consolidated) multi: Taproot Assets Ark integration (PoC) Aug 12, 2026
@darioAnongba
darioAnongba marked this pull request as draft August 12, 2026 09:42
Add a versioned sealed-package container and bind optional Taproot Asset
roots to OOR input and recipient policies. Persist input and
start-message roots so durable retries can reconstruct the same composed
scripts.
Extend submit and offline-receive messages with optional asset roots and
the versioned sealed-package container. Keep Bitcoin-only payloads
backward compatible and validate asset bytes at the typed request
boundary.
Accept externally committed OOR graphs, validate them before signing,
and retain sealed Taproot Asset packages through durable actor messages,
FSM states, snapshots, and byte-identical submit retries.
Store the optional Taproot Asset commitment root with each VTXO and
derive composed policy scripts and spend control blocks for OOR,
forfeit, and unilateral exit paths. Exclude asset-bearing rows from
generic Bitcoin coin selection until rounds carry asset transitions.
Carry recipient commitment roots and sealed asset transition packages
through offline receive, durable actor snapshots, VTXO materialization,
and OOR artifact persistence. Preserve Bitcoin-only package behavior
while requiring asset-aware stores for asset sessions.
Add a proof-selected public intent and a restart-safe preparer seam so
Wavelength can commit an asset graph before its durable OOR actor signs
it. Keep Bitcoin-only sends unchanged and reject unsupported change,
missing adapters, and tampered results before actor submission.
Adapt the merged tap-sdk custom-anchor boundary to the Wavelength OOR
graph. Verify the selected tapd inventory, commit both transitions, and
persist outcome-aware replay state before the actor can sign.
Keep tapd credentials in the client daemon and install the adapter only
after explicit configuration. Preserve embedded config isolation and
document the PoC recovery and custody boundaries.
Document why ordinary round boarding cannot create the first
asset-bearing VTXO and record the fully rebased client validation
results.
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.
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.
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.
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.
Refreshing an asset-bearing VTXO forfeits the leaf and asks the same
units back as a fixed-amount asset VTXO request, so the round reissues
them through its asset transition. The wallet descriptor and the RPC
preview learn the asset identity; the blanket refresh rejection is
lifted for explicitly named targets while the catch-all selection
still skips them.
The proof-of-funds input for a forfeited VTXO derived the default
timeout path, whose witness program is the bare policy output. An
asset leaf pays to the policy branched with its asset root, so the
proof needs the composed control block, the same way boarding
disclosures already build theirs.
The round store's ClientVTXO loader dropped the Taproot Asset columns,
so a forfeited asset leaf looked like plain sats to the join-auth
builder and its proof spent the wrong witness program.
The unroll puts the composed leaf on chain; once its CSV delay
matures, the claim completes the leaf's sealed lineage into a
confirmed proof file, spends the anchor through the exit leaf, and
commits one transition into a fresh tapd-owned anchor. Script and
internal keys both come from tapd, so the units become ordinary,
spendable wallet balance when the claim confirms.
An asset leaf's worth is the assets riding on it, so the feasibility
verdict priced in carrier sats refused every asset exit as
uneconomical.
The wallet script plan derives a fresh asset script key at commit
time, so a preview's commitment root never matches the commit and the
anchor output script is only knowable afterwards. Ship a placeholder
template and adopt the committed transaction's script instead.
Publishing verifies the transition against tapd's own proof archive,
so the leaf's confirmed lineage is imported first. The OP_TRUE script
key is declared from its own witness material: script and control
block carry the internal key and tapscript root. Declaring at claim
time is correct, since the anchor is on chain under the owner's
exclusive control and the claim spends it in the same flow.
The asset metadata checks pushed the incoming handler's Receive past
the function-length limit. The script derivation and descriptor
assembly move into a helper; behavior is unchanged.
@darioAnongba
darioAnongba force-pushed the darioAnongba/taproot-assets-oor-runtime branch from 0dfe1af to 66b879c Compare August 12, 2026 10:24
The asset integration tests stop skipping when no tapd override is
set, so the default image becomes what CI actually runs. v0.8.1 is
the latest release; the tests need the CommitVirtualPsbts
transition-proof v1 support and stay red until a release carries it.
That red is deliberate: a silent skip hides lost coverage, a failing
job names it.
MuSig2CreateSession sorts the signer slice it is handed in place, so
concurrent sessions over nodes sharing one backing array race on the
sort. The aggregation paths already copy through sortableCopy; the
session path now does the same.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant