diff --git a/docs/developer-deep-dive.md b/docs/developer-deep-dive.md
new file mode 100644
index 000000000..5e52cb9c4
--- /dev/null
+++ b/docs/developer-deep-dive.md
@@ -0,0 +1,1175 @@
+# The Taproot Assets Protocol: A Deep Technical Dive
+
+NOTE: This is a document initially created by an agentic LLM analyzing the code
+base and then reviewed and edited by a human.
+This document covers broader topics a developer might need or want to know
+about when developing apps using Taproot Assets.
+Another, [similar document covering the specifics around transaction
+flows](./developer-transaction-flow.md) exists as well and there is some overlap
+between these two documents. This document's target audience is developers
+_using_ and interacting with `tapd`.
+
+All links to code are based on the commit
+[`45586345`](https://github.com/lightninglabs/taproot-assets/tree/45586345).
+
+## Introduction
+
+The Taproot Assets Protocol (TAP) is a system that enables the creation,
+management, and transfer of digital assets on the Bitcoin blockchain.
+Unlike traditional token protocols that rely on separate blockchains or complex
+smart contracts, Taproot Assets leverages Bitcoin's native Taproot upgrade to
+embed asset commitments directly into Bitcoin transactions. This approach
+provides the security and finality of Bitcoin while enabling rich asset
+functionality.
+
+At its core, the protocol solves a fundamental challenge: how to represent
+arbitrary assets on Bitcoin without modifying the base protocol. The solution
+involves sophisticated cryptographic commitments, merkle trees, and a carefully
+designed proof system that allows assets to be verified independently while
+remaining anchored to Bitcoin's proof-of-work security.
+
+## Understanding the Foundation: MS-SMT Trees
+
+### What is an MS-SMT?
+
+The Merkle Sum Sparse Merkle Tree (MS-SMT) is the fundamental data structure
+that powers Taproot Assets. Unlike regular merkle trees that only commit to
+data, MS-SMTs also commit to numerical sums at each node. This property is
+crucial for preventing asset inflation - a critical security requirement for any
+asset protocol.
+
+In the context of Taproot Assets, MS-SMTs serve multiple purposes. They provide
+cryptographic commitments to asset ownership, enable efficient proofs of
+existence and non-existence, and most importantly, enforce conservation laws
+that prevent assets from being created or destroyed outside of authorized
+minting operations.
+
+The "sparse" aspect is equally important. Since the tree can theoretically hold
+2^256 entries (one for each possible key), but in practice only contains a few
+assets, the sparse structure allows efficient storage and proof generation
+without needing to materialize the entire tree. As noted in
+[`mssmt/tree.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/mssmt/tree.go),
+the implementation uses a compacted representation that only stores non-empty
+nodes.
+
+### Why MS-SMT Over Other Structures?
+
+The choice of MS-SMT over simpler structures like regular merkle trees or even
+just hash commitments is deliberate and serves several critical purposes:
+
+**Sum Preservation**: The sum property ensures that assets cannot be inflated
+during transfers. When an asset is split, the sum of the output amounts must
+equal the input amount. This is enforced mathematically by the tree structure
+itself, not just by validation rules. This check is implemented in
+[`vm/vm.go:433-445`](https://github.com/lightninglabs/taproot-assets/blob/45586345/vm/vm.go#L433-L445):
+
+```go
+// Enforce that assets aren't being inflated.
+ctxb := context.Background()
+treeRoot, err := inputTree.Root(ctxb)
+if err != nil {
+ return err
+}
+if treeRoot.NodeSum() !=
+ uint64(virtualTx.TxOut[0].Value) {
+
+ return newErrInner(ErrAmountMismatch, fmt.Errorf("expected "+
+ "output value=%v, got=%v", treeRoot.NodeSum(),
+ virtualTx.TxOut[0].Value))
+}
+```
+
+**Efficient Proofs**: Sparse merkle trees enable both inclusion proofs (proving
+an asset exists) and exclusion proofs (proving an asset doesn't exist). This is
+essential for preventing double-spends and ensuring uniqueness of ownership.
+
+**Scalability**: The sparse structure means the tree size grows logarithmically
+with the number of assets, not linearly. This makes the protocol viable even
+with millions of assets.
+
+## The Tree Hierarchy: A Multi-Level Architecture
+
+Taproot Assets employs a two-level tree architecture.
+Understanding this hierarchy is crucial to grasping how the protocol maintains
+security while enabling complex asset operations.
+
+### Level 1: The TAP Commitment Tree
+
+The outermost tree is the TAP Commitment, defined in
+[`commitment/tap.go:82-85`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/tap.go#L82-L85).
+This tree serves as the root anchor for all assets in a Bitcoin UTXO:
+
+```go
+// TapCommitment represents the outer MS-SMT within the Taproot Asset protocol
+// committing to a set of asset commitments. Asset commitments are
+// keyed by their `asset_group_key` or `asset_id` otherwise.
+```
+
+This tree's keys are either asset IDs (for unique assets) or group keys (for
+fungible assets that can be reissued). Each leaf in this tree is itself a
+commitment to another tree - the Asset Commitment.
+
+### Level 2: Asset Commitments
+
+Each leaf in the TAP Commitment tree contains an Asset Commitment
+([`commitment/asset.go:55-58`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/asset.go#L55-L58)):
+
+```go
+// AssetCommitment represents the inner MS-SMT within the Taproot Asset protocol
+// committing to a set of assets under the same ID/group. Assets within this
+// tree are keyed by their `asset_script_key`.
+```
+
+This inner tree contains the actual assets (asset outputs or asset UTXOs), with
+each asset keyed by its script key (the public key that controls spending).
+
+This two-level system allows for easy sum-based checks of how many asset units
+are represented in a single commitment.
+
+But because the script key is the uniquely identifying key in the Asset
+Commitment tree, this also requires the script keys to be unique within a single
+Asset Commitment tree to prevent asset leaf collisions.
+
+This uniqueness constraint is checked by `tapd` to avoid different asset-level
+outputs to be clobbered together by accident.
+
+### The Split Commitment Tree: A Special Case
+
+When assets are transferred with change (a split), a temporary third tree type
+comes into play - the Split Commitment tree
+([`commitment/split.go:108-122`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/split.go#L108-L122)).
+This tree is fundamentally different and completely independent of the
+persistent TAP and Asset commitment trees:
+
+```go
+type SplitCommitment struct {
+ // PrevAssets is the set of asset inputs being split.
+ PrevAssets InputSet
+
+ // RootAsset is the root asset resulting after the creation of split
+ // assets containing the SplitCommitmentRoot.
+ RootAsset *asset.Asset
+
+ // SplitAssets is the set of asset splits within the on-chain
+ // transaction committed to within the split commitment MS-SMT.
+ SplitAssets SplitSet
+
+ // tree is the MS-SMT committing to all of the asset splits above.
+ tree mssmt.Tree
+}
+```
+
+The split tree exists only within the root asset of a split operation and serves
+as a proof structure showing how an asset was divided. This is crucial for
+non-interactive transfers where the recipient needs to independently verify the
+transfer's validity.
+
+## Visualizing the Tree Structure
+
+```mermaid
+graph TD
+ A[Bitcoin UTXO] --> B[TAP Commitment Tree]
+ B --> C[Leaf: Asset Commitment 1
Key: AssetID_A]
+ B --> D[Leaf: Asset Commitment 2
Key: AssetID_B]
+
+ C --> E[Asset Commitment Tree
AssetID_A]
+ D --> F[Asset Commitment Tree
AssetID_B]
+
+ E --> G[Asset: 100 units
Key: Alice's ScriptKey]
+ E --> H[Asset: 50 units
Key: Bob's ScriptKey]
+
+ F --> I[Asset: 200 units
Key: Charlie's ScriptKey]
+```
+
+## The Proof System: Ensuring Validity Without Trust
+
+The proof system in Taproot Assets is what enables trustless verification.
+Unlike traditional database systems where you trust the database operator, or
+even blockchain systems where you trust the consensus, Taproot Assets proofs
+allow mathematical verification of asset properties.
+
+### Components of a Proof
+
+A complete Taproot Asset proof consists of several components, each serving a
+specific purpose in the verification chain:
+
+**Asset Proof**: The serialized asset itself, including its genesis information,
+amount, script key, and any witness data. This is the core of what's being
+proven.
+
+**Inclusion Proofs**: Merkle proofs showing the asset's inclusion in both the
+Asset Commitment tree and the TAP Commitment tree. These proofs consist of
+sibling hashes that allow reconstruction of the root hash.
+
+**Split Commitment Proof**: For assets that resulted from a split, this includes
+the split tree root and the merkle proof from the specific output to that root.
+This proves the asset came from a valid split operation that did not destroy or
+create assets.
+
+**Bitcoin Anchor Proof**: The Bitcoin transaction and merkle proof showing the
+TAP Commitment is embedded in a confirmed Bitcoin transaction. This anchors the
+entire proof to Bitcoin's security.
+
+### Proof Verification Process
+
+When verifying a proof, the process works backwards from the Bitcoin blockchain
+down to the specific asset. First, the Bitcoin transaction is verified to ensure
+it's confirmed and contains the expected commitment. Then, the TAP commitment
+root is extracted and verified against the provided merkle proofs. Next, the
+asset commitment is verified within the TAP tree. Finally, the specific asset is
+verified within its asset commitment tree.
+
+This layered verification ensures that an asset cannot be forged - it must trace
+back through valid proofs all the way to a confirmed Bitcoin transaction. The
+verification logic is implemented in the `proof` package, with the core
+validation happening in
+[`proof/verifier.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/proof/verifier.go).
+
+## Understanding Commitments
+
+Commitments in Taproot Assets serve as cryptographic anchors that bind assets to
+specific states. There are several types of commitments, each serving a
+different purpose in the protocol.
+
+### TAP Commitments
+
+The TAP Commitment is the root commitment that gets embedded in Bitcoin
+transactions. As defined in
+[`commitment/tap.go:78-82`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/tap.go#L78-L82),
+it commits to all assets in a UTXO:
+
+```go
+// TapCommitment represents the outer MS-SMT within the Taproot Asset protocol
+// committing to a set of asset commitments.
+```
+
+This commitment becomes part of the Bitcoin taproot output through a tapscript
+leaf. The commitment script includes a version byte, a marker, the tree root
+hash, and the sum of all assets. This structure is defined in
+[`commitment/tap.go:64-72`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/tap.go#L64-L72).
+
+### Asset Commitments
+
+Asset Commitments group assets of the same type together. Each asset commitment
+is itself an MS-SMT where assets are keyed by their script keys. This allows
+multiple owners to hold the same asset type within the same Bitcoin UTXO, though
+this is typically only seen during batch operations or in special protocols.
+
+The commitment preserves important properties like asset type consistency and
+sum verification. The validation in
+[`commitment/asset.go:91-150`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/asset.go#L91-L150)
+ensures all assets in a commitment share the same genesis and type.
+
+### Split Commitments
+
+Split Commitments are special commitments that prove how an asset was divided.
+They're created during split operations and embedded in the root asset (often
+the change output). The split commitment tree uses locators as keys, where each
+locator uniquely identifies an output:
+
+```go
+// From commitment/split.go:78-87
+func (l SplitLocator) Hash() [sha256.Size]byte {
+ h := sha256.New()
+ binary.Write(h, binary.BigEndian, l.OutputIndex)
+ h.Write(l.AssetID[:])
+ h.Write(l.ScriptKey.SchnorrSerialized())
+ return *(*[sha256.Size]byte)(h.Sum(nil))
+}
+```
+
+*Source:
+[`commitment/split.go:78-87`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/split.go#L78-L87)*
+
+## Transfer Types: Interactive vs Non-Interactive
+
+The protocol supports two fundamental transfer modes that differ primarily in
+how asset outputs are constructed. This distinction is often confused with proof
+distribution mechanisms, but they are separate concerns.
+
+### The Core Distinction: Output Construction
+
+**Interactive vs non-interactive is fundamentally about asset output
+construction, not proof distribution.**
+
+- **Non-interactive transfers**: Only exist to facilitate TAP address based
+ on-chain transfers (for V0 and V1 TAP addresses). See [Non-Interactive
+ Transfers and TAP Addresses](#non-interactive-transfers-and-tap-addresses)
+ section below.
+- **Interactive transfers**: Can use direct asset reassignment for full-value
+ transfers as there is no requirement for predictability on the outputs. It is
+ assumed that the recipient eventually learns (either through the TAP address
+ V2 authenticated mailbox mechanism or through an out-of-band application
+ process) the proof or the locator information to fetch a proof from a proof
+ courier.
+
+### Interactive Transfers
+
+Interactive transfers occur when both sender and receiver coordinate directly to
+create the transfer or at least communicate to learn about the location of the
+final proofs. The key characteristic is that they don't require split
+commitments for full-value transfers.
+
+The detection logic in
+[`tapsend/send.go:1379-1406`](https://github.com/lightninglabs/taproot-assets/blob/45586345/tapsend/send.go#L1379-L1406)
+determines if a transfer is a full-value interactive send:
+
+```go
+// interactiveFullValueSend returns true (and the index of the recipient output)
+// if there is exactly one output that spends the input fully and interactively
+func interactiveFullValueSend(totalInputAmount uint64,
+ outputs []*tappsbt.VOutput) (int, bool) {
+
+ // ... identify non-split-root outputs ...
+ fullValueInteractiveSend := numRecipientOutputs == 1 &&
+ outputs[recipientIndex].Amount == totalInputAmount &&
+ outputs[recipientIndex].Interactive
+ return recipientIndex, fullValueInteractiveSend
+}
+```
+
+When detected, the asset is directly reassigned
+[`tapsend/send.go:506-569`](https://github.com/lightninglabs/taproot-assets/blob/45586345/tapsend/send.go#L506-L569):
+
+```go
+// If we have an interactive full value send, we don't need a tomb stone
+// at all.
+if isFullValueInteractiveSend {
+ vOut.Asset = firstInput.Asset().Copy()
+ vOut.Asset.Amount = inputsAmountSum
+ vOut.Asset.ScriptKey = vOut.ScriptKey
+
+ // We are done, since we don't need to create a split commitment.
+ return nil
+}
+```
+
+**Key Properties:**
+- No split commitment required for full-value transfers
+- No tombstone outputs needed
+- Both parties potentially participate in transaction construction but minimally
+ the receiver learns about the proof or the proof location through direct
+ communication between the sender and receiver
+- Proofs are generated and distributed as part of the coordinated process
+
+### Non-Interactive Transfers and TAP Addresses
+
+Taproot Asset (TAP) addresses of version 0 and version 1 use non-interactive
+transfers, which require a
+specific output structure for predictability. **The receiver MUST receive a split
+output** - this is the fundamental requirement that drives the complexity.
+
+**Why Split Outputs Are Required:**
+
+To allow a receiver to fully predict the on-chain Taproot output key
+(p2tr address) in order for them to watch the chain, they must be able to
+predict the full asset leaf and with that the asset and TAP commitment trees
+all the way up to the Taproot output key. For this predictability to work, an
+asset output cannot contain any signatures, as they wouldn't be predictable.
+So a split output is used, which can be constructed in a predictable manner: the
+address must contain the expected asset ID, amount and script key. And the
+asset output sent to an address receiver _MUST_ use a single split output
+(with the split commitment being trimmed from the asset commitment to allow
+predictability). The consequence of this is that even when an input asset is
+fully spent (full-value transfer), a change output must be created that houses
+the root asset (that contains the authorizing signature in the witness and the
+split commitment root). Such a zero-value root asset output is also called a
+"tombstone" output, since it doesn't carry any asset value and only serves as
+a cryptographic proof holder. Such "tombstone" outputs _MUST_ use the NUMS
+script key and are allowed to be dropped from the commitment in a future
+spend.
+
+**The Tombstone Solution:**
+
+Tombstones serve as "root outputs" that carry the witness (signature) for the
+split. Even in full-value transfers, you need:
+
+1. **Split output** (for the receiver) - contains the asset but no witness
+2. **Tombstone output** (root) - contains zero value but carries the witness
+
+The tombstone mechanism, defined in
+[`asset/asset.go:159-164`](https://github.com/lightninglabs/taproot-assets/blob/45586345/asset/asset.go#L159-L164),
+uses a Nothing-Up-My-Sleeve (NUMS) key:
+
+```go
+// ScriptKeyTombstone is the script key type used for assets that are
+// not spendable and have been marked as tombstones. This is only the
+// case for zero-value assets that result from a non-interactive (TAP
+// address) send where no change was left over.
+```
+
+The NUMS key itself is defined at
+[`asset/asset.go:264-277`](https://github.com/lightninglabs/taproot-assets/blob/45586345/asset/asset.go#L264-L277):
+
+```go
+// NUMSBytes is the NUMS point we'll use for un-spendable script keys.
+// It was generated via a try-and-increment approach using the phrase
+// "taproot-assets" with SHA2-256.
+NUMSBytes, _ = hex.DecodeString(
+ "027c79b9b26e463895eef5679d8558942c86c4ad2233adef01bc3e6d540b3653fe",
+)
+NUMSPubKey, _ = btcec.ParsePubKey(NUMSBytes)
+```
+
+**Tombstones are about predictability, not proving non-ownership.** They're a
+technical requirement for the TAP address commitment structure to work
+correctly.
+
+### Proof Distribution: TAP Addresses vs plain Script Keys
+
+**Proof distribution is separate from the interactive/non-interactive
+distinction.**
+What matters is whether you're receiving to:
+
+1. **TAP Address**: Automatic proof fetching from proof courier
+2. **Plain Script Key**: Manual proof import required
+
+**The Three States of Proof Existence:**
+
+Proofs can exist in three different places, each with different implications:
+
+1. **Transfer Table**: Incomplete proof "suffixes" before Bitcoin confirmation,
+ created by a sender in their own node, only relevant while transaction
+ remains unconfirmed.
+2. **Universe Tree**: Indexed proof repository for proof lookup and syncing.
+ Every node has a universe tree and syncing trees is the process for learning
+ about new assets and transfers. Having a proof in a local universe tree does
+ not imply asset ownership.
+3. **Proof Archive/Database**: Node "owns" the asset and can spend it. The
+ entries in the `assets` database table are asset-level UTXO that the node
+ can spend directly or at least knows some keys for (e.g. multisig).
+
+The state machine for transfers is defined in
+[`tapfreighter/chain_porter.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/tapfreighter/chain_porter.go#L1650-L1730)
+with states progressing through:
+- `SendStateStorePreBroadcast`
+- `SendStateBroadcast`
+- `SendStateWaitTxConf`
+- `SendStateStorePostAnchorTxConf`
+- `SendStateTransferProofs`
+- `SendStateComplete`
+
+**Transfer Completion Flow:**
+
+When a transfer completes, the sender:
+1. Takes incomplete proof suffixes from the transfers table
+2. Completes them by adding Bitcoin block header information
+3. Decides what to do with each proof:
+ - **Known script key** → Insert into local proof archive ("owned")
+ - **Unknown script key** → Push to Universe as proof courier
+
+**Special Case: Self-Transfers with Custom Scripts**
+
+If Alice sends to herself using a custom script (like multisig), she needs to
+manually import/register the proof because:
+- The custom script isn't automatically recognized as "her own"
+- Without manual import/registration, the proof stays in the transfer table
+- She has the proof data but doesn't "own" it until it's in the proof archive
+
+### Transfer Completion Detection
+
+**Completion detection depends on the receiving method, not the transfer type:**
+
+**For TAP Address Receives (Non-Interactive Only):**
+- Monitor address events through `QueryAddrEvents`
+- Track status progression as defined in
+ [`address/event.go:20-42`](https://github.com/lightninglabs/taproot-assets/blob/45586345/address/event.go#L20-L42):
+ - `StatusTransactionDetected` (0): Transaction seen but unconfirmed
+ - `StatusTransactionConfirmed` (1): Transaction confirmed, awaiting proof
+ - `StatusProofReceived` (2): Proof received, being validated
+ - `StatusCompleted` (3): Full custody taken, asset spendable
+- Completion logic in
+ [`tapgarden/custodian.go:1438-1456`](https://github.com/lightninglabs/taproot-assets/blob/45586345/tapgarden/custodian.go#L1438-L1456)
+- See [the technical documentation of the `Custodian` for more
+ details](../tapgarden/README.md).
+
+**For plain Script Key Receives (Interactive):**
+- Monitor `SubscribeSendEvents` for `SendStateComplete` (sender side)
+- Watch for Bitcoin transaction confirmation
+- **Manual transfer registration/proof import required** using
+ `RegisterTransfer` (see
+ [`rpcserver.go:9899`](https://github.com/lightninglabs/taproot-assets/blob/45586345/rpcserver.go#L9899))
+- Verify balance updates with `ListAssets`
+
+The proof import/registration determines ownership - having proof data in the
+universe doesn't mean ownership until it's in the proof archive.
+
+**Critical Insight: Proof Possession vs Ownership**
+
+Having proof data in the universe tree doesn't mean you "own" the asset.
+Ownership requires the proof to be in your proof archive/database and the script
+key to be spendable by the node. This is why manual import/registration is
+sometimes needed even when you have the proof data in the universe tree.
+
+**Example Scenarios:**
+
+1. **Normal TAP Address Receive**: Automatic proof fetching and import via
+ address events
+2. **Interactive Transfer**: Proofs generated during coordination, automatic
+ ownership detection for known script keys that go back to the sender (change
+ outputs)
+3. **Self-Transfer with Custom Script (e.g., multisig)**: Manual
+ `RegisterTransfer` needed because custom scripts aren't auto-detected as
+ "owned"
+4. **Multi-party Protocols**: Each party needs to manually import/register
+ proofs for their outputs
+
+**Test Examples:**
+- Interactive full value send:
+- [`itest/psbt_test.go:300`](https://github.com/lightninglabs/taproot-assets/blob/45586345/itest/psbt_test.go#L300)
+- Interactive split send:
+- [`itest/psbt_test.go:739`](https://github.com/lightninglabs/taproot-assets/blob/45586345/itest/psbt_test.go#L739)
+
+### Transfer Validation
+
+Both transfer types undergo rigorous validation through the Taproot Assets
+Virtual Machine (VM). The VM, implemented in
+[`vm/vm.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/vm/vm.go),
+performs several critical checks:
+
+**Amount Conservation**: The VM ensures that the sum of inputs equals the sum of
+outputs, preventing inflation or deflation of assets. This check is performed
+for every transfer, regardless of type.
+
+**Witness Validation**: Each input must have a valid witness (signature) from
+the previous owner. The VM constructs a virtual Bitcoin transaction and uses
+Bitcoin's script validation engine to verify these witnesses.
+
+**Split Commitment Validation**: For non-interactive transfers, the VM
+additionally validates the split commitment proofs, ensuring each output can
+prove its lineage from the input.
+
+## The Virtual Transaction Model
+
+One of the most elegant aspects of Taproot Assets is its use of virtual
+transactions. These are Bitcoin-like transactions that exist only within the
+protocol but leverage Bitcoin's mature script validation system.
+
+### Construction of Virtual Transactions
+
+Virtual transactions are created by the `tapscript` package and represent asset
+state transitions as if they were Bitcoin transactions. The virtual transaction
+has inputs corresponding to the assets being spent and outputs representing the
+new asset states.
+
+The construction process, defined in
+[`tapscript/tx.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/tapscript/tx.go),
+maps asset inputs and
+outputs to virtual Bitcoin inputs and outputs. The amounts in the virtual
+transaction represent asset amounts, not Bitcoin amounts, allowing the Bitcoin
+script system to validate asset transfers.
+
+### Witness Validation Using Bitcoin Script
+
+By representing asset transfers as virtual Bitcoin transactions, Taproot Assets
+can leverage Bitcoin's battle-tested script validation engine. Witnesses
+(signatures) are validated using the same elliptic curve cryptography and script
+evaluation rules as Bitcoin.
+
+This approach provides several benefits. It ensures compatibility with existing
+Bitcoin signing devices and wallets, inherits the security properties of
+Bitcoin's script system, and allows for complex spending conditions using
+Bitcoin script primitives.
+
+### Proof Distribution Flow
+
+When an asset is transferred, especially in non-interactive mode, the proof
+distribution typically follows this flow:
+
+1. The sender completes the transfer and generates proofs
+2. The sender uploads the proof to relevant Universe servers
+3. The receiver detects the on-chain transfer (for non-interactive)
+4. The receiver queries Universe servers for the proof
+5. The receiver validates the proof and takes custody of the asset
+
+This flow ensures that proofs are available when needed while maintaining the
+decentralized nature of the protocol.
+
+## Practical Example: A Complete Asset Transfer
+
+Let's walk through a complete example of Alice sending 60 of her 100 units to
+Bob via a non-interactive transfer, demonstrating how all these concepts work
+together.
+
+### Initial State
+
+Alice owns 100 units of AssetX, held in a Bitcoin UTXO. The commitment structure
+looks like this:
+
+```mermaid
+graph TD
+ A[Bitcoin UTXO - Alice] --> B[TAP Commitment]
+ B --> C[Asset Commitment
AssetX]
+ C --> D[Asset: 100 units
Alice's ScriptKey]
+```
+
+### Step 1: Creating the Transfer
+
+Alice initiates a transfer to Bob's Taproot Asset address, which encodes Bob's
+script key and the desired amount. The transfer creation process involves
+several substeps.
+
+First, Alice's wallet constructs a virtual packet representing the transfer.
+This packet specifies 60 units to Bob and implicitly 40 units change to Alice.
+Since the amount is being split, a split commitment will be created.
+
+### Step 2: Split Commitment Creation
+
+The split commitment creation, handled by
+[`commitment/split.go:142-303`](https://github.com/lightninglabs/taproot-assets/blob/45586345/commitment/split.go#L142-L303),
+creates an MS-SMT with two entries:
+
+```mermaid
+graph TD
+ A[Split Commitment Tree] --> B[Root Hash]
+ B --> C[Locator₀: Alice Change
40 units]
+ B --> D[Locator₁: Bob's Output
60 units]
+```
+
+Each output gets a merkle proof linking it to the split root. The root hash is
+embedded in Alice's change output.
+
+### Step 3: Creating Bitcoin Transaction
+
+Alice creates a Bitcoin transaction with two outputs:
+
+**Output 0** (Alice's change):
+- Contains TAP commitment with 40-unit asset
+- Asset output is a split root asset that includes the SplitCommitmentRoot and
+ transfer witness (signature)
+- Uses Alice's new script key
+
+**Output 1** (Bob's transfer):
+- Contains TAP commitment with 60-unit asset
+- Asset output is a split output that includes the split commitment proof
+ linking to root asset
+- Uses Bob's script key from the address
+
+### Step 4: Proof Generation and Distribution
+
+After the Bitcoin transaction confirms, Alice generates complete proofs for
+Bob's output. These proofs include:
+
+- The Bitcoin transaction and confirmation proof
+- The TAP commitment merkle proof
+- The asset commitment merkle proof
+- The split commitment proof
+- The witness (signature) authorizing the transfer
+
+Alice uploads these proofs to Universe servers, making them available for Bob to
+retrieve.
+
+### Step 5: Bob's Receipt and Verification
+
+Bob detects the on-chain transfer by watching for transactions to his address's
+taproot output key. He then queries Universe servers for proofs matching his
+script key.
+
+Upon receiving the proofs, Bob's wallet performs complete verification:
+1. Verifies the Bitcoin transaction is confirmed
+2. Validates the TAP commitment merkle proof
+3. Validates the asset commitment merkle proof
+4. Verifies the split commitment proof
+5. Validates Alice's signature on the virtual transaction
+
+Once all checks pass, Bob has cryptographic proof of ownership and can spend the
+asset in future transactions.
+
+### Final State
+
+After the transfer, the asset distribution looks like this:
+
+```mermaid
+graph TD
+ A1[Bitcoin UTXO - Output 0] --> B1[TAP Commitment]
+ B1 --> C1[Asset Commitment
AssetX]
+ C1 --> D1[Asset: 40 units
Alice's New ScriptKey
+SplitCommitmentRoot]
+
+ A2[Bitcoin UTXO - Output 1] --> B2[TAP Commitment]
+ B2 --> C2[Asset Commitment
AssetX]
+ C2 --> D2[Asset: 60 units
Bob's ScriptKey
+Split Proof]
+```
+
+## Advanced Topics
+
+### Group Assets and Reissuance
+
+Group assets introduce the ability to issue additional units of an asset after
+the initial genesis. Assets in a group share a group key but may have different
+asset IDs. The group key serves as the commitment key in the TAP tree, allowing
+all assets in the group to be committed together.
+
+Group membership is proven through a witness that demonstrates the asset was
+issued by the group key holder. This enables controlled supply expansion while
+maintaining verifiability.
+
+### Collectibles and NFTs
+
+Collectible assets (NFTs) have special rules in the protocol. They cannot be
+divided - any transfer must be for the full amount (always 1). They can only
+have one input in a transfer, preventing merging. These restrictions are
+enforced by the VM during validation.
+
+The protocol's support for collectibles demonstrates its flexibility in handling
+different asset types while maintaining the same security guarantees.
+
+### Multi-Asset Transfers
+
+The protocol supports transferring multiple assets in a single Bitcoin
+transaction. Each asset gets its own virtual packet and proof structure, but
+they all anchor to the same Bitcoin transaction. This enables efficient batch
+operations and complex protocols like atomic swaps.
+
+### Integration with Lightning Network
+
+Taproot Assets can be integrated with Lightning Network through Taproot Asset
+Channels. These channels allow instant, off-chain transfers of assets using the
+same HTLC mechanisms as regular Lightning. The integration leverages the virtual
+transaction model, making assets compatible with Lightning's existing
+infrastructure.
+
+## Security Considerations
+
+### Inflation Resistance
+
+The protocol's use of MS-SMT trees provides mathematical guarantees against
+inflation. Every transfer must preserve the sum of assets, enforced by both the
+tree structure and VM validation. This makes unauthorized asset creation
+impossible without breaking the cryptographic commitments.
+
+### Proof Non-Malleability
+
+Proofs in Taproot Assets are non-malleable due to their merkle tree structure.
+Any modification to a proof would change the root hash, making it invalid. This
+ensures proofs can be safely shared and stored without risk of tampering.
+
+### Bitcoin Security Inheritance
+
+By anchoring all commitments to Bitcoin transactions, Taproot Assets inherits
+Bitcoin's security properties. Asset transfers have the same finality guarantees
+as Bitcoin transactions. Reorgs affect assets the same way they affect Bitcoin.
+The cost of attacking assets is equivalent to attacking Bitcoin itself.
+
+### Privacy Considerations
+
+While the protocol provides some privacy through the use of taproot outputs,
+there are limitations. On-chain observers can see Bitcoin transactions but not
+asset details. Universe servers may learn about asset transfers when proofs are
+uploaded. Address reuse should be avoided to prevent linking transfers.
+
+## RPC Usage Guide: Practical Transfer Workflows
+
+This section provides concrete RPC workflows for different transfer scenarios,
+based on actual implementation patterns from the codebase.
+
+### Core RPC Categories
+
+**Address Management:**
+- `NewAddr` - Create TAP addresses for receiving assets
+- `DecodeAddr` - Decode and inspect TAP addresses
+- `NextScriptKey` - Generate script keys for interactive transfers
+
+**Transfer Execution:**
+- `SendAsset` - Simple non-interactive send to TAP address
+- `FundVirtualPsbt` - Create funded virtual PSBT for interactive transfers
+- `SignVirtualPsbt` - Sign virtual PSBT
+- `AnchorVirtualPsbts` - Anchor signed PSBTs to Bitcoin transaction
+
+**Proof Management:**
+- `ExportProof` - Export the full proof file for a specific asset transfer from
+ the proof archive
+- `ImportProofs` (deprecated!) / `RegisterTransfer` - Import proofs to take
+ ownership
+- `DecodeProof` - Inspect proof contents
+
+**Universe tree interaction:**
+- `QueryProof` - Query a single proof in the universe tree
+- `InsertProof` - Insert a single proof into the universe tree
+
+**Monitoring:**
+- `SubscribeSendEvents` - Monitor transfer state machine
+- `SubscribeReceiveEvents` - Monitor incoming transfers
+- `ListAssets` - Check asset balances
+
+### Workflow 1: Non-Interactive Send (TAP Address)
+
+This is the simplest transfer type - sending to a TAP address.
+
+**Step 1: Receiver Creates Address**
+```go
+// Receiver creates a TAP address
+addr, err := receiver.NewAddr(ctx, &taprpc.NewAddrRequest{
+ AssetId: assetId,
+ Amt: amount,
+})
+```
+
+**Step 2: Sender Executes Transfer**
+```go
+// Sender sends to the TAP address
+sendResp, err := sender.SendAsset(ctx, &taprpc.SendAssetRequest{
+ TapAddrs: []string{addr.Encoded},
+})
+```
+
+**Step 3: Monitor Completion**
+```go
+// Sender monitors for completion
+stream, _ := sender.SubscribeSendEvents(ctx, &taprpc.SubscribeSendEventsRequest{})
+for {
+ event, _ := stream.Recv()
+ if event.SendState == "SendStateComplete" {
+ // Transfer complete
+ break
+ }
+}
+```
+
+**Step 4: Receiver Detects Completion**
+
+The receiver automatically detects and imports proofs via address events. No
+manual action needed. Monitor with:
+```go
+// Monitor address events (automatic for TAP addresses)
+events, _ := receiver.QueryAddrEvents(ctx, &taprpc.QueryAddrEventsRequest{
+ AddrTaprootOutputKey: addr.TaprootOutputKey,
+})
+// Look for StatusCompleted (value 3)
+```
+
+**Code Reference:**
+[`itest/send_test.go:101-109`](https://github.com/lightninglabs/taproot-assets/blob/45586345/itest/send_test.go#L101-L109)
+
+### Workflow 2: Interactive Transfer (plain Script Key)
+
+Interactive transfers require coordination between sender and receiver.
+
+**Step 1: Generate Keys**
+```go
+// Receiver generates script key and internal key
+scriptKeyResp, _ := receiver.NextScriptKey(
+ ctx, &wrpc.NextScriptKeyRequest{
+ KeyFamily: taprpc.TaprootAssetsKeyFamily,
+ },
+)
+internalKeyResp, _ := receiver.NextInternalKey(
+ ctx, &wrpc.NextInternalKeyRequest{
+ KeyFamily: taprpc.TaprootAssetsKeyFamily,
+ },
+)
+```
+
+**Step 2: Create Virtual Packet**
+
+The sender creates a virtual packet for the transfer:
+```go
+// Using the tappsbt package (not direct RPC)
+vPkt := tappsbt.ForInteractiveSend(
+ assetId, amount, scriptKey, 0, 0, 0,
+ internalKey, asset.V0, chainParams,
+)
+```
+
+**Step 3: Fund the PSBT**
+```go
+fundResp, _ := sender.FundVirtualPsbt(
+ ctx, &wrpc.FundVirtualPsbtRequest{
+ Template: &wrpc.FundVirtualPsbtRequest_Raw{
+ Raw: &wrpc.TxTemplate{
+ Recipients: vPkt.Outputs,
+ },
+ },
+ },
+)
+```
+
+**Step 4: Sign the PSBT**
+```go
+signResp, _ := sender.SignVirtualPsbt(
+ ctx, &wrpc.SignVirtualPsbtRequest{
+ FundedPsbt: fundResp.FundedPsbt,
+ },
+)
+```
+
+**Step 5: Anchor to Bitcoin**
+```go
+sendResp, _ := sender.AnchorVirtualPsbts(
+ ctx, &wrpc.AnchorVirtualPsbtsRequest{
+ VirtualPsbts: [][]byte{signResp.SignedPsbt},
+ },
+)
+```
+
+**Step 6: Manual Proof Transfer**
+
+For interactive transfers, proofs must be manually transferred:
+```go
+// Sender exports the proof
+resp, err := src.ExportProof(ctxt, &taprpc.ExportProofRequest{
+ AssetId: assetID,
+ ScriptKey: scriptKey,
+ Outpoint: outpoint,
+})
+...
+
+// Receiver imports the proof, using the RegisterTransfer function
+registerResp, err := dst.RegisterTransfer(
+ ctxb, &taprpc.RegisterTransferRequest{
+ AssetId: proofAsset.AssetGenesis.AssetId,
+ GroupKey: groupKey,
+ ScriptKey: proofAsset.ScriptKey,
+ Outpoint: &taprpc.OutPoint{
+ Txid: op.Hash[:],
+ OutputIndex: op.Index,
+ },
+ },
+)
+```
+
+**Code Reference:**
+[`itest/psbt_test.go:640-696`](https://github.com/lightninglabs/taproot-assets/blob/45586345/itest/addrs_test.go#L878)
+
+### Workflow 3: Custom Script (Multisig) Transfer
+
+For custom scripts like multisig, the receiver won't auto-detect ownership.
+
+**Step 1-5: Same as Interactive Transfer**
+
+Follow the interactive transfer steps to create and execute the transfer.
+
+**Step 6: Critical - Manual Import/Registration Required**
+
+Even if the receiver participated in creating the transfer, they must manually
+import/register the proof because custom scripts aren't auto-detected:
+
+```go
+// CRITICAL: Even though receiver has the proof data,
+// they must import/register it to claim ownership
+registerResp, err := dst.RegisterTransfer(
+ ctxb, &taprpc.RegisterTransferRequest{
+ AssetId: proofAsset.AssetGenesis.AssetId,
+ GroupKey: groupKey,
+ ScriptKey: proofAsset.ScriptKey,
+ Outpoint: &taprpc.OutPoint{
+ Txid: op.Hash[:],
+ OutputIndex: op.Index,
+ },
+ },
+)
+// Without this, the asset won't appear in ListAssets
+```
+
+**Why Manual Import?**
+
+The system can't automatically recognize custom scripts as "owned" by the node.
+The proof exists but isn't in the proof archive until imported/registered.
+
+### Workflow 4: Monitoring Transfer Progress
+
+**For Senders:**
+```go
+stream, _ := sender.SubscribeSendEvents(
+ ctx, &taprpc.SubscribeSendEventsRequest{
+ FilterScriptKey: scriptKey, // Optional filter
+ },
+)
+
+for {
+ event, _ := stream.Recv()
+ switch event.SendState {
+ case "SendStateBroadcast":
+ // Transaction broadcast
+ case "SendStateWaitTxConf":
+ // Waiting for confirmation
+ case "SendStateTransferProofs":
+ // Transferring proofs
+ case "SendStateComplete":
+ // Transfer complete
+ return
+ }
+}
+```
+
+**For Receivers (TAP Address only):**
+```go
+stream, _ := receiver.SubscribeReceiveEvents(
+ ctx, &taprpc.SubscribeReceiveEventsRequest{
+ FilterAddr: addr.Encoded,
+ },
+)
+
+for {
+ event, _ := stream.Recv()
+ if event.Status == address.StatusCompleted {
+ // Asset received and spendable
+ return
+ }
+}
+```
+
+### Key Decision Points
+
+**When to use SendAsset vs PSBTs?**
+- `SendAsset`: Simple transfers to TAP addresses
+- PSBTs: Interactive transfers, custom scripts, complex protocols
+
+**When is manual RegisterTransfer needed?**
+- Always for interactive transfers to script keys
+- Always for custom scripts (even self-transfers)
+- Never for TAP address receives (automatic)
+
+**How to detect transfer completion?**
+- Senders: `SubscribeSendEvents` → `SendStateComplete`
+- TAP receivers: Address events → `StatusCompleted`
+- Script key receivers: Manual check after `RegisterTransfer`
+
+### Common Pitfalls
+
+1. **Forgetting Manual Import/Registration**: Interactive transfers always need
+ manual proof import/registration by the receiver
+
+2. **Custom Script Self-Transfers**: Even sending to yourself with custom
+ scripts requires manual import/registration
+
+3. **Proof vs Ownership**: Having proof data ≠ ownership. Must be in proof
+ archive via `RegisterTransfer`
+
+4. **Address Reuse**: Avoid reusing TAP addresses for privacy
+
+### Testing Your Implementation
+
+The integration tests provide excellent examples:
+- Non-interactive: [`itest/send_test.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/itest/send_test.go)
+- Interactive PSBTs: [`itest/psbt_test.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/itest/psbt_test.go)
+- Address handling: [`itest/addrs_test.go`](https://github.com/lightninglabs/taproot-assets/blob/45586345/itest/addrs_test.go)
+
+## Implementation Best Practices
+
+### Wallet Integration
+
+When implementing Taproot Assets in a wallet, several considerations are
+important:
+
+**Key Management**: Assets use the same key derivation as Bitcoin (BIP-32/84),
+but with dedicated derivation paths. Wallets should support both script keys
+(for ownership) and internal keys (for Bitcoin outputs).
+
+**Proof Storage**: Wallets must store proofs for owned assets, as losing proofs
+means losing the ability to spend assets. Proofs should be backed up separately
+from keys.
+
+**Universe Interaction**: Wallets should support multiple Universe servers for
+redundancy. They should verify proofs from untrusted Universes and be able to
+serve proofs for completed transfers.
+
+### Application Development
+
+Applications building on Taproot Assets should consider:
+
+**Transfer Mode Selection**: Use interactive transfers for real-time
+applications like exchanges. Use non-interactive transfers for traditional
+payment flows using TAP addresses. Consider hybrid approaches for optimal UX.
+
+**Proof Management**: Implement robust proof storage and retrieval. Cache
+frequently accessed proofs. Implement proof garbage collection for old
+transfers.
+
+**Error Handling**: Handle Bitcoin fee estimation and management. Implement
+retry logic for failed transfers. Provide clear error messages for validation
+failures.
+
+## Custom Script Keys and Advanced Transfer Scenarios
+
+### Understanding Script Key Recognition
+
+Taproot Assets wallets automatically recognize script keys that follow standard
+patterns (BIP-86), but custom script keys require explicit declaration. This
+becomes critical in advanced scenarios like deposit management systems.
+
+### When DeclareScriptKey is Required
+
+The `DeclareScriptKey` RPC is needed when:
+
+- **Custom script keys** (like OP_TRUE) are used that tapd cannot automatically
+ recognize
+- Script keys contain **non-standard scripts or tweaking patterns**
+- External systems create script keys that bypass normal wallet derivation
+
+Without declaration, assets sent to custom script keys won't be recognized as
+local assets, appearing as if they belong to external parties.
+
+### When RegisterTransfer is Required
+
+The `RegisterTransfer` RPC is needed for:
+
+- **Incoming transfers** using script keys not derived by the local wallet
+- Transfers that **bypass normal tapd receiving flows** (like deposit
+ management)
+- **Manual asset import** where the asset exists on-chain but not in local
+ storage
+
+From [`rpcserver.go:9965`](https://github.com/lightninglabs/taproot-assets/blob/45586345/rpcserver.go#L9965),
+`RegisterTransfer` explicitly
+validates that script keys are already known via `FetchScriptKey` and suggests
+declaring unknown keys.
+
+### Script Key Serialization Pitfalls
+
+A critical issue exists with custom script key handling due to serialization
+format mismatches:
+
+**The Problem**: The `SetAssetSpent` database query in
+[`tapdb/assets_store.go`](tapdb/assets_store.go) requires exact script key
+matching between registration and sweep operations. Custom script keys generated
+by functions like `htlc.CreateOpTrueLeaf()` may not match database lookups due
+to different serialization formats.
+
+**The Manifestation**: During asset sweeps, the system fails with
+`"sql: no rows in result set"` when trying to mark assets as spent, even though
+the asset exists.
+
+**Root Cause**: Script key parity byte handling differs between:
+- Registration via `DeclareScriptKey` (expects 33-byte compressed format)
+- Database lookup during sweep (uses internal representation)
+- Standard compressed public key format includes parity that affects matching
+
+### Workaround for Custom Script Keys
+
+When using custom script keys in deposit management scenarios, the following
+transformation resolves the serialization mismatch:
+
+```go
+// Transform script key through schnorr parsing to resolve parity mismatch
+sKey, err := schnorr.ParsePubKey(
+ schnorr.SerializePubKey(opTrueScriptKey.PubKey), // Strip parity byte
+)
+if err != nil {
+ return err
+}
+scriptKeyBytes := sKey.SerializeCompressed() // Re-serialize as 33-byte
+
+// Use the transformed key for RegisterTransfer
+_, err = tapClient.RegisterTransfer(ctx, &taprpc.RegisterTransferRequest{
+ AssetId: assetID[:],
+ ScriptKey: scriptKeyBytes, // Use transformed key
+ Outpoint: outpoint,
+})
+```
+
+This creates a 33-byte public key with the same parity bit representation that
+matches what the database expects during sweep operations.
+
+### Best Practices for Custom Scripts
+
+1. **Always declare custom script keys** before creating assets or addresses
+ that use them
+2. **Use consistent serialization** between declaration and transfer
+ registration
+3. **Test sweep operations** to ensure script key matching works correctly
+4. **Consider the schnorr transformation workaround** for OP_TRUE and similar
+ patterns
+
+This pattern is particularly important for **deposit management systems** where
+servers own deposits but clients fund them, requiring careful coordination of
+script key handling between parties.
diff --git a/docs/developer-transaction-flow.md b/docs/developer-transaction-flow.md
new file mode 100644
index 000000000..04cf66b1c
--- /dev/null
+++ b/docs/developer-transaction-flow.md
@@ -0,0 +1,1053 @@
+# Taproot Assets Transaction Flow: A Comprehensive Technical Guide
+
+NOTE: This is a document initially created by an agentic LLM analyzing the code
+base and then reviewed and edited by a human.
+This document covers the specifics of transaction flows. Another, [similar
+document covering more broad topics](./developer-deep-dive.md) exists as
+well and there is some overlap between these two documents. This document's
+target audience is developers working on and contributing to `tapd`'s code
+itself.
+
+This document serves as the definitive technical reference for understanding the
+Taproot Assets transaction flow architecture, documenting the critical
+invariants, subtle interactions, and implementation details that future
+engineers must understand to maintain and extend this system safely.
+
+All links to code are based on the commit
+[`45586345`](https://github.com/lightninglabs/taproot-assets/tree/45586345).
+
+## Table of Contents
+
+1. [Philosophical Overview](#philosophical-overview)
+2. [The Dual-Layer Architecture](#the-dual-layer-architecture)
+3. [Asset Lifecycle and State Management](#asset-lifecycle-and-state-management)
+4. [Passive Assets: The Hidden Complexity](#passive-assets-the-hidden-complexity)
+5. [Split Commitments: Enabling Divisibility](#split-commitments-enabling-divisibility)
+6. [Tombstones: The Zero-Value Anchors](#tombstones-the-zero-value-anchors)
+7. [Interactive vs Non-Interactive Sends: A Deep Dive](#interactive-vs-non-interactive-sends-a-deep-dive)
+8. [Address Versions: V1 vs V2 Evolution](#address-versions-v1-vs-v2-evolution)
+9. [V2 Script Key Generation: The Art of Preventing Address Reuse](#v2-script-key-generation-the-art-of-preventing-address-reuse)
+10. [Transaction Flow: From Intent to Settlement](#transaction-flow-from-intent-to-settlement)
+11. [Critical Invariants and Common Pitfalls](#critical-invariants-and-common-pitfalls)
+12. [Implementation Reference](#implementation-reference)
+
+## Philosophical Overview
+
+Taproot Assets represents a fundamental architectural decision: instead of
+modifying Bitcoin's consensus rules to support assets, we build a parallel state
+machine that commits to Bitcoin for ordering and finality. This design
+philosophy permeates every aspect of the system and explains many implementation
+choices that might otherwise seem arbitrary.
+
+The protocol operates on two distinct but interconnected layers. The virtual
+layer handles all asset-specific logic through virtual transactions that never
+touch the blockchain directly. The anchor layer provides the bridge to Bitcoin,
+embedding cryptographic commitments that prove asset state transitions without
+revealing asset details to Bitcoin nodes. This separation is not merely
+conceptual, it's reflected in our code structure, with distinct packages
+handling virtual (`tappsbt`, `asset`) versus anchor (`tapfreighter`, `tapsend`)
+operations.
+
+## The Dual-Layer Architecture
+
+### Virtual Transactions: The Asset Logic Layer
+
+Virtual transactions represent the pure expression of asset transfer intent.
+When you see a `VPacket` in the code (`tappsbt/interface.go`), you're looking at
+a structure that exists only in memory and proof files, never on-chain. These
+virtual transactions contain the complete asset transfer logic: which assets are
+being spent (inputs), where they're going (outputs), and the cryptographic
+witnesses that authorize these transfers.
+
+```mermaid
+graph TB
+ subgraph "Virtual Layer (Never On-Chain)"
+ VTX[Virtual Transaction/VPacket]
+ VIN[Virtual Inputs
- Asset Proofs
- Witness Data]
+ VOUT[Virtual Outputs
- New Assets
- Script Keys]
+ VSIG[Virtual Signatures
- Asset-Level Auth]
+ end
+
+ subgraph "Anchor Layer (Bitcoin)"
+ BTX[Bitcoin Transaction]
+ BIN[Bitcoin Inputs
- Spend UTXOs]
+ BOUT[Bitcoin Outputs
- Taproot Commitments]
+ BSIG[Bitcoin Signatures
- UTXO Auth]
+ end
+
+ VIN --> VTX
+ VTX --> VOUT
+ VSIG --> VTX
+
+ VTX -.->|Commitment| BOUT
+ BIN --> BTX
+ BTX --> BOUT
+ BSIG --> BTX
+
+ style VTX fill:#e1f5fe
+ style BTX fill:#fff3e0
+```
+
+The virtual transaction construction begins in `tappsbt/create.go` with
+functions like `FromAddresses()` and `FromProofs()`. These functions don't just
+create data structures, they establish the foundation for the entire transfer.
+Every virtual input must reference a valid previous asset state (through
+proofs), and every output must specify exactly how that asset should exist after
+the transfer.
+
+### Anchor Transactions: The Settlement Layer
+
+Anchor transactions serve as the bridge between our virtual asset world and
+Bitcoin's UTXO model. The name "anchor" is deliberate: these transactions anchor
+virtual state transitions to Bitcoin's immutable ledger. The anchor transaction
+creation process (`tapsend/send.go:CreateAnchorTx()`) performs a delicate
+dance: it must embed Taproot Asset commitments within standard Bitcoin outputs
+while maintaining compatibility with existing Bitcoin infrastructure.
+
+The commitment structure uses Taproot's script path spending to hide our asset
+commitments. When Bitcoin nodes see our anchor transactions, they see standard
+Taproot outputs. Only those with the asset proofs can derive the commitment
+structure and verify the assets within. This is implemented through the
+`commitment` package, which builds Merkle Sum Sparse Merkle Trees (MS-SMT) that
+commit to all assets in an output while preserving value conservation
+properties.
+
+## Asset Lifecycle and State Management
+
+### The Asset State Machine
+
+Assets in our system follow a well-defined state machine that governs their
+lifecycle from creation to potential destruction. Understanding these states is
+crucial for avoiding the bugs that have plagued earlier implementations.
+
+```mermaid
+stateDiagram
+ [*] --> Minted: Genesis Transaction
+ Minted --> Active: First Transfer
+ Active --> Split: Division Required
+ Split --> ActiveSplit: Root Asset (has SplitCommitmentRoot)
+ Split --> PassiveSplit: Split Outputs (have SplitCommitment witness)
+ Active --> Passive: Shares UTXO with Transferred Asset
+ Passive --> Active: Becomes Primary Transfer Asset
+ PassiveSplit --> Passive: Not Being Transferred
+ ActiveSplit --> Active: Spending Split Root
+ Active --> Transferred: Sent to New Owner
+ Transferred --> Imported: Cross-Node Receipt
+ Imported --> Active: Ready for Next Transfer
+ Active --> Burned: Destroyed (NUMS key)
+ Burned --> [*]
+
+ note right of Passive: CRITICAL - Must preserve ALL witness data during re-anchoring
+ note right of Imported: CRITICAL - Must store split commitment roots during import
+ note right of Split: Split commitment data flows through entire lifecycle
+```
+
+Each state transition has specific requirements and invariants that must be
+maintained. The genesis state establishes the asset's identity through the
+genesis point (`tapdb/assets_store.go:upsertGenesisPoint()`).
+
+### Active vs Passive Assets: A Critical Distinction
+
+The distinction between active and passive assets is fundamental to
+understanding the protocol's efficiency and complexity. Active assets are those
+explicitly involved in the current transfer—they're the reason the transaction
+exists. Passive assets, however, are innocent bystanders that happen to share
+the same UTXO.
+
+Consider a UTXO containing three different assets: Alice wants to send Asset A
+to Bob, but Assets B and C also live in that UTXO. Asset A becomes the active
+asset, while B and C are passive. When Alice spends the UTXO to transfer Asset
+A, she must also handle Assets B and C—they need new homes (re-anchoring) or
+they'd be destroyed.
+
+The re-anchoring process occurs in
+`tapfreighter/wallet.go:createPassivePacket()`. This function creates virtual
+transactions that move passive assets from their current UTXO to a new one,
+preserving their exact state.
+
+## Passive Assets: The Hidden Complexity
+
+### The Re-Anchoring Process
+
+When designing the passive asset system, the goal was efficiency: allow
+multiple assets to coexist in a single UTXO without requiring separate
+transactions for each. This decision introduced significant complexity that
+manifests throughout the codebase.
+
+The passive asset creation flow begins when we identify which assets aren't
+involved in the active transfer. The `tapsend` package provides
+`RemovePacketsFromCommitment()` to extract passive assets from a commitment
+after removing active transfers. These passive assets must then be re-anchored
+to maintain their existence.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Wallet
+ participant PassiveHandler as Passive Handler
+ participant Commitment as Commitment Tree
+ participant Anchor as Anchor Output
+
+ User->>Wallet: Transfer Asset A
+ Wallet->>Commitment: Extract All Assets from UTXO
+ Commitment->>Wallet: Returns [A, B, C]
+ Wallet->>PassiveHandler: A is active, handle B and C
+
+ critical Re-Anchoring Process
+ PassiveHandler->>PassiveHandler: Create passive packet for B
+ PassiveHandler->>PassiveHandler: Create passive packet for C
+ PassiveHandler->>PassiveHandler: MUST preserve split commitments!
+ end
+
+ PassiveHandler->>Anchor: Attach B and C to new output
+ Anchor->>Anchor: Rebuild commitment with all assets
+```
+
+The critical part to understand here is that a re-anchored asset chan change its
+type when being re-anchored. If a passive asset previously was a split output,
+it then becomes a normal root asset output, which on the asset-level looks like
+a normal interactive full-value transfer.
+
+Also, during the re-anchoring transfer, the same script key is re-used but a new
+signature is created.
+
+This also implies that any custom-script assets can never be passive assets, as
+the internal re-anchoring logic can only sign normal BIP-86 script keys.
+
+That means for custom script assets, all assets in an UTXO that is being spent
+must be signed by the external application.
+
+### Anchor Output Selection
+
+Choosing where to place passive assets involves a decision
+implemented in `determinePassiveAssetAnchorOutput()`. The function prefers, in
+order:
+
+1. **Split root outputs**: These typically represent change and are controlled
+ by the sender
+2. **Outputs with new script keys**: These indicate transfers to self
+3. **New anchor outputs**: Created only when necessary
+
+This hierarchy ensures passive assets remain accessible to their owner while
+minimizing on-chain footprint.
+
+## Split Commitments: Enabling Divisibility
+
+### The Mathematics of Asset Splits
+
+Split commitments solve a fundamental problem: how do you prove that dividing an
+asset conserves its total value? The solution employs Merkle Sum Sparse Merkle
+Trees (MS-SMT), a data structure that combines the proof properties of Merkle
+trees with value summation.
+
+When an asset splits, the process creates a cryptographic proof that the sum of
+outputs equals the input. This happens in
+`commitment/split.go:NewSplitCommitment()`, which constructs the MS-SMT and
+generates proofs for each output.
+
+```mermaid
+graph TD
+ Input[Input Asset: 100 units] --> RootCalc[Calculate Split Root]
+ RootCalc --> Tree[MS-SMT Construction]
+
+ Tree --> Out1[Output 1: 60 units
+ Inclusion Proof]
+ Tree --> Out2[Output 2: 40 units
+ Inclusion Proof]
+
+ subgraph "Split Commitment Structure"
+ Root[Root Asset
SplitCommitmentRoot = tree.Root]
+ Root --> NodeHash[32-byte Node Hash]
+ Root --> NodeSum[Sum = 100]
+ end
+
+ subgraph "Split Witnesses"
+ W1[Witness 1
SplitCommitment.Proof]
+ W2[Witness 2
SplitCommitment.Proof]
+ end
+
+ Out1 -.->|validates against| Root
+ Out2 -.->|validates against| Root
+ W1 --> Out1
+ W2 --> Out2
+```
+
+The root asset (the one being split) receives a `SplitCommitmentRoot` containing
+the tree root hash and sum. Each split output gets a `SplitCommitment` in its
+witness, containing the proof that validates against this root.
+
+### Split Commitment Validation
+
+The validation logic in `vm/vm.go:validateSplit()` enforces several invariants:
+
+1. **Conservation**: Sum of split outputs must equal input amount
+2. **Proof Validity**: Each split's proof must validate against the root
+3. **Type Consistency**: All splits must be the same asset type
+4. **Script Key Rules**: Zero-value splits must use unspendable keys
+
+These checks ensure splits maintain asset integrity while enabling flexible
+division.
+
+## Tombstones: The Zero-Value Anchors
+
+### Understanding Tombstones
+
+Tombstones are a critical but often misunderstood component of the Taproot
+Assets protocol. They solve a fundamental problem: how to maintain valid
+commitment structures in non-interactive transfers when sending entire asset
+amounts without requiring unnecessary change output assets.
+
+A tombstone is a zero-value asset output that serves as an anchor for the split
+commitment root and transfer witness in full-value, non-interactive transfers.
+The name "tombstone" reflects its nature—it marks the "death" of the sender's
+ownership while preserving the cryptographic proof structure needed for
+validation.
+
+```mermaid
+graph TD
+ subgraph "Without Tombstone (Interactive)"
+ A1[Input: 100 units] --> B1[Output: 100 units to Bob]
+ A1 -.-> NO[No change needed, no output]
+ style NO fill:#e8f5e9
+ end
+
+ subgraph "With Tombstone (Non-Interactive)"
+ A2[Input: 100 units] --> B2[Output: 100 units to Bob]
+ A2 --> T[Tombstone: 0 units
Split Root Anchor]
+ style T fill:#ffebee
+ end
+
+ note1[Interactive: Direct coordination, split root anchor goes to recipient]
+ note2[Non-interactive: Address-based, requires split root anchor going back to sender]
+
+ B1 -.-> note1
+ T -.-> note2
+```
+
+### Why Tombstones Are Necessary
+
+The necessity of tombstones stems from the protocol's split commitment
+architecture. In non-interactive transfers:
+
+1. **Split Commitment Requirement**: The protocol requires a split commitment to
+ prove value conservation and to achieve predictability of split outputs
+ required for V0 and V1 on-chain TAP address receives
+2. **Root Asset Location**: The split commitment root must live somewhere in the
+ transaction outputs
+4. **Zero-Value Problem**: When sending the full amount, there's no natural
+ change output
+
+The tombstone solves this by creating a zero-value output that the sender
+controls, which can house the split commitment root. This is implemented in
+`tapsend/allocation.go:642`:
+
+```go
+// Create a zero-amount tombstone output for the split root, if
+// there is no change.
+vOut.Type = tappsbt.TypeSplitRoot
+vOut.Amount = 0
+vOut.ScriptKey = asset.NUMSScriptKey // Un-spendable key
+```
+
+### Tombstone Creation Logic
+
+The decision to create a tombstone follows a specific logic tree
+(`tapsend/allocation.go:634-644`):
+
+```mermaid
+flowchart TD
+ Start[Transfer Initiated] --> Check1{Is Interactive?}
+ Check1 -->|Yes| NoTomb[No Tombstone Needed]
+ Check1 -->|No| Check2{Full Amount Send?}
+ Check2 -->|No| Change[Regular Change Output]
+ Check2 -->|Yes| Check3{Is Split Root?}
+ Check3 -->|No| NoTomb
+ Check3 -->|Yes| Tomb[Create Tombstone]
+
+ Tomb --> Details[Zero Amount
NUMS Script Key
Type: SplitRoot]
+
+ style NoTomb fill:#e8f5e9
+ style Tomb fill:#ffebee
+ style Details fill:#fff3e0
+```
+
+### Tombstone Script Keys
+
+Tombstones use a special "Nothing Up My Sleeve" (NUMS) script key that makes
+them provably un-spendable. This is crucial for security—tombstones should never
+hold value and should never be spendable:
+
+```go
+// From asset/asset.go
+var NUMSScriptKey = ScriptKey{
+ PubKey: NUMSPubKey, // Provably un-spendable public key
+ Type: ScriptKeyTombstone,
+}
+```
+
+The un-spendable nature is verified in `tapfreighter/chain_porter.go:1877-1880`:
+
+```go
+unSpendable, _ := scriptKey.IsUnSpendable()
+if unSpendable {
+ setScriptKeyType(vOut, asset.ScriptKeyTombstone)
+}
+```
+
+### Tombstones in Different Address Versions
+
+The tombstone requirement varies by address version:
+
+- **V0/V1 Addresses**: Always require tombstones for full-value non-interactive
+ sends
+- **V2 Addresses**: Support interactive transfers through send manifests,
+ avoiding tombstone requirement
+- **Burns**: Never require tombstones (always interactive, as there is no
+ "receiver")
+- **Channel Operations**: Never require tombstones (always interactive)
+
+### Common Tombstone Pitfalls
+
+1. **Missing Tombstone Detection**: Failing to create tombstones in
+ non-interactive full sends causes validation failures
+2. **Incorrect Type Assignment**: Not marking tombstone outputs with
+ `ScriptKeyTombstone` type
+3. **Spendable Tombstones**: Using regular script keys instead of NUMS keys
+4. **Tombstone in Interactive Transfers**: Creating unnecessary tombstones in
+ interactive transfers
+
+## Interactive vs Non-Interactive Sends: A Deep Dive
+
+### The Fundamental Distinction
+
+The interactive vs non-interactive distinction is one of the most important
+architectural decisions in Taproot Assets. It determines transaction structure,
+efficiency, and user experience.
+
+**Interactive Sends** occur when the sender and receiver coordinate during the
+transfer:
+- Direct communication between parties
+- Receiver provides their script key directly
+- Proof delivery can happen through direct communication or using proof courier
+ mechanisms
+- More efficient on-chain footprint since no tombstones are required for
+ full-value sends
+
+**Non-Interactive Sends** occur when sending to a V0/V1 TAP address without
+receiver coordination:
+- Sender only has the receiver's TAP address
+- Must use proof courier for delivery
+- Requires split commitment structure
+- Always creates change/tombstone outputs
+
+
+### Implementation Details
+
+#### Interactive Send Construction
+
+Interactive sends are created via `tappsbt/create.go:ForInteractiveSend()`:
+
+```go
+func ForInteractiveSend(id asset.ID, amount uint64, scriptKey asset.ScriptKey,
+ anchorOutIdx uint32) (*VPacket, error) {
+
+ vPkt := &VPacket{
+ Inputs: []*VInput{},
+ Outputs: []*VOutput{{
+ Amount: amount,
+ AssetVersion: asset.V0,
+ Type: TypeSimple,
+ Interactive: true, // Key distinction
+ AnchorOutputIndex: anchorOutIdx,
+ ScriptKey: scriptKey,
+ }},
+ }
+ // No automatic change output creation
+ return vPkt, nil
+}
+```
+
+#### Non-Interactive Send Construction
+
+Non-interactive sends via `tappsbt/create.go:FromAddresses()`:
+
+```go
+func FromAddresses(spendAddr []*address.Tap, changeIdx uint32) (*VPacket, error) {
+ outputs := make([]*VOutput, 0, len(spendAddr)+1)
+
+ // Create outputs for each address
+ for idx, addr := range spendAddr {
+ out := &VOutput{
+ Amount: addr.Amount,
+ Interactive: false, // Non-interactive by definition
+ AnchorOutputIndex: uint32(idx),
+ ScriptKey: addr.ScriptKey,
+ ProofDeliveryAddress: addr.ProofCourierAddr, // Required for delivery
+ }
+ outputs = append(outputs, out)
+ }
+
+ // Always pre-allocate change/tombstone output
+ outputs = append(outputs, &VOutput{
+ Type: TypeSplitRoot,
+ AnchorOutputIndex: changeIdx,
+ Interactive: false,
+ })
+
+ return &VPacket{Outputs: outputs}, nil
+}
+```
+
+### The VOutIsInteractive Predicate
+
+The protocol uses a specific predicate to determine if an output is interactive
+(`tappsbt/interface.go:130`):
+
+```go
+// VOutIsInteractive returns true if the virtual output is interactive.
+VOutIsInteractive = func(o *VOutput) bool {
+ switch {
+ // Burns are always interactive
+ case o.Type.IsBurn():
+ return true
+
+ // Explicitly marked interactive outputs
+ case o.Interactive:
+ return true
+
+ // Split roots for passive assets are interactive
+ case o.Type == TypePassiveSplitRoot:
+ return true
+
+ default:
+ return false
+ }
+}
+```
+
+### Use Case Matrix
+
+| Use Case | Type | Reason |
+|----------|------|--------|
+| Address-based payment | Non-Interactive | No receiver coordination |
+| Lightning channel funding | Interactive | Both parties online |
+| Asset burn | Interactive | Sender controls entire flow |
+| Exchange deposit | Non-Interactive | Exchange provides address |
+| P2P trade | Interactive | Direct negotiation |
+| Passive asset re-anchor | Interactive | Self-transfer |
+| Donation | Non-Interactive | One-way transfer |
+
+## Address Versions: V0/V1 vs V2 Evolution
+
+### The Journey from Fixed Keys to Reusable Addresses
+
+The evolution of Taproot Asset addresses tells a story of learning from
+real-world usage patterns and adapting to user needs. When V0/V1 addresses
+launched as the first production version they embodied a
+straightforward approach: one address, one script key, one asset type, no
+interaction needed. This simplicity served initial adoption well, but as the
+ecosystem matured, fundamental limitations emerged that threatened to constrain
+the protocol's potential.
+
+The most glaring issue with V0/V1 addresses was their handling of script keys.
+Every V0/V1 address contains a fixed script key (`address/address.go`) that's
+used directly for all transfers. While conceptually simple, this design created
+a specific problem: Repeated transfers to the same TAP address created
+identical on-chain outputs, making transactions trivially linkable and
+impacting privacy.
+
+Beyond the privacy implications V0/V1 addresses suffered from another constraint
+that hampered real-world usage: they bound specific amounts to the address
+itself. When generating a V0/V1 address, you had to specify exactly how many
+units of an asset should be sent to that address. This meant that invoice systems
+needed to generate a new address for every payment amount, donation platforms
+couldn't create open-ended contribution addresses, and exchanges had to either
+dictate deposit amounts or generate addresses on-demand for each user's intended
+deposit. The rigid coupling between address and amount made V0/V1 addresses feel
+more like single-use payment requests than true reusable destinations.
+
+The third and most impactful limitation of V0/V1 addresses was that they could
+only be used for specific asset IDs. Meaning that fungible grouped assets were
+not supported.
+
+V2 addresses emerged from recognizing that address reuse wasn't just a
+nice-to-have feature—it was essential for practical adoption. And major
+stablecoin issuers would issue grouped fungible assets.
+So V2 addresses can be "zero-value" addresses that can receive any assets from
+a specific group — the amount isn't bound to the address but instead specified
+by the sender at transfer time. And any asset ID the sender owns of the asset
+group specified in the address could be sent, not just a single one.
+This flexibility means a charity can post a single V2 address and receive
+donations of any amount, exchanges can display permanent deposit addresses
+without knowing in advance how much users will send, and payment systems can use
+addresses as true persistent identifiers rather than one-time payment codes.
+
+### The Architectural Shift: From Script Keys to Internal Keys
+
+Where V0/V1 addresses contain a script key that's used directly, V2 addresses
+contain what we call an internal key—a key that serves as the seed for deriving
+unique script keys for each asset type. This shift might seem subtle, but it
+fundamentally changes the protocol's capabilities. The `ScriptKeyForAssetID()`
+function demonstrates this:
+
+```go
+func (a *Tap) ScriptKeyForAssetID(assetID asset.ID) (*btcec.PublicKey, error) {
+ if a.Version != V2 {
+ return &a.ScriptKey, nil // V1: Direct usage, no derivation
+ }
+
+ // V2: Each asset gets its own derived script key
+ scriptKey, err := asset.DeriveUniqueScriptKey(
+ a.ScriptKey, // Now an internal key for derivation
+ assetID,
+ asset.ScriptKeyDerivationUniquePedersen,
+ )
+ return scriptKey.PubKey, nil
+}
+```
+
+This derivation mechanism means that a single V2 address can safely receive any
+number of different asset IDs without collision. Each asset ID produces a
+unique script key through Pedersen commitments, creating isolated namespaces
+within the same address.
+
+### Send Manifests: The Missing Piece
+
+The support for arbitrary asset IDs created a new challenge: if every transfer
+produces unpredictable on-chain outputs, how does the receiver know where to
+look for their assets? V0/V1's predictable outputs meant receivers could simply
+scan for their calculated Taproot outputs. V2's dynamic outputs broke this model
+entirely.
+
+Enter send manifests, V2's solution to the discoverability problem. The
+`UsesSendManifests()` function (`address/address.go:391-397`) signals that an
+address expects to receive detailed manifests describing incoming transfers.
+These manifests, transmitted through the authmailbox proof courier protocol,
+contain all the information needed to locate and claim assets: the exact
+on-chain location, the asset IDs and amounts and the derived script keys used.
+From that, the proof data needed for validation could be fetched by the
+receiver, using the same proof courier universe server.
+
+```go
+func (a *Tap) UsesSendManifests() bool {
+ return a.Version == V2 // Only V2 addresses use manifests
+}
+```
+
+This manifest-based approach transforms what could have been a limitation into
+a powerful feature. Because receivers get explicit notification of incoming
+transfers, V2 addresses enable complex multi-asset atomic operations. A single
+transaction can transfer different assets to the same V2 address, with the
+manifest ensuring the receiver can claim everything. This opens doors for
+sophisticated DeFi protocols, atomic swaps, and batch payment systems that were
+impossible with V1's constraints.
+
+### Commitment Compatibility: The Hidden Complexity
+
+The versioning story extends beyond addresses to the commitment structures
+themselves. V0 addresses, being from the experimental era, could use either V0
+or V1 commitment versions—the protocol can't determine which without examining
+the actual commitment tree. V1 and V2 addresses, however, standardized on
+commitment V2, providing consistency and predictability:
+
+```go
+func CommitmentVersion(vers Version) (*commitment.TapCommitmentVersion, error) {
+ switch vers {
+ case V0:
+ return nil, nil // Ambiguous: could be either V0 or V1 commitment
+ case V1, V2:
+ return fn.Ptr(commitment.TapCommitmentV2), nil // Standardized
+ default:
+ return nil, ErrUnknownVersion
+ }
+}
+```
+
+This standardization matters because commitment versions affect how assets are
+encoded in the Taproot tree structure. V2 commitments introduced optimizations
+and additional metadata fields that improve verification efficiency. By tying
+address versions to commitment versions, the protocol ensures that senders and
+receivers have compatible expectations about data structures.
+
+### Real-World Impact: Exchange Integration
+
+The practical benefits of V2 addresses become clear when considering exchange
+integration. With V1 addresses, an exchange needed to generate a new address for
+each asset type a user might deposit, then monitor multiple addresses per user.
+This created operational complexity: more addresses to track, more database
+entries to maintain, and confusion for users who might send assets to the wrong
+address.
+
+V2 addresses eliminate this friction entirely. An exchange can generate a single
+V2 address per user, display it prominently in the deposit interface, and accept
+any supported Taproot Asset without modification. The address can be reused
+indefinitely without privacy concerns thanks to the random alt leaf mechanism.
+When a deposit arrives, the send manifest provides all necessary information for
+crediting the user's account. This simplicity has accelerated exchange
+adoption—operators appreciate the reduced complexity, and users enjoy the
+familiar experience of having a stable deposit address.
+
+### The Migration Path: Coexistence and Compatibility
+
+The transition from V1 to V2 addresses showcases thoughtful protocol evolution.
+Rather than forcing a hard migration, the protocol supports both versions
+simultaneously. V2 nodes can receive from V1 addresses, ensuring backward
+compatibility. When a V1 sender transfers to a V2 address, the system
+automatically handles the impedance mismatch—the V1 sender creates traditional
+proofs while the V2 receiver expects manifests, and the proof courier bridges
+this gap.
+
+This coexistence strategy reflects a deeper philosophy: protocol upgrades should
+enhance capabilities without breaking existing deployments. Organizations that
+invested in V1 infrastructure can continue operating while gradually migrating
+to V2 at their own pace. New deployments can start directly with V2, benefiting
+from its advanced features without dealing with legacy constraints.
+
+### Looking Forward: The Foundation for Future Innovation
+
+V2 addresses represent more than just an incremental improvement—they establish
+a foundation for future protocol enhancements. The manifest system creates a
+communication channel between senders and receivers that can carry additional
+metadata. The unique script key derivation enables each asset type to have
+distinct transfer rules. The separation between internal keys and derived script
+keys allows for sophisticated key management schemes, including hardware wallet
+integration and threshold signatures.
+
+As the Taproot Assets ecosystem continues to evolve, V2 addresses provide the
+flexibility needed for innovation while maintaining the security and simplicity
+users expect. They demonstrate that thoughtful protocol design can solve
+real-world problems without compromising on fundamental principles. The journey
+from V1's fixed keys to V2's dynamic derivation illustrates how protocols can
+learn from deployment experience and adapt to serve their users better.
+
+## V2 Script Key Generation: The Art of Preventing Address Reuse
+
+### The Address Reuse Problem and Its Solution
+
+Traditional Bitcoin wallets discourage address reuse for privacy reasons—every
+payment to the same address creates an obvious on-chain link. V2 Taproot Asset
+addresses solve this elegantly through a sophisticated key derivation mechanism
+that ensures every transfer creates a unique on-chain footprint, even when the
+same address is used repeatedly.
+
+The magic happens through a two-layer approach implemented in
+`prepareV2Outputs()` (`tapfreighter/wallet.go:300`). When a sender prepares a
+transfer to a V2 address, the system doesn't simply use the address's script key
+directly. Instead, it embarks on a cryptographic journey that transforms the
+receiver's internal key into something unique for each asset and each transfer.
+
+### Understanding Pedersen Commitments in Script Keys
+
+At the heart of V2's uniqueness guarantee lies the Pedersen commitment, a
+cryptographic primitive that allows us to commit to data in a way that's both
+binding and hiding. The `DeriveUniqueScriptKey()` function
+(`asset/asset.go:1276`) implements this through what might initially seem like
+cryptographic alchemy but is actually an elegant solution to a complex problem.
+
+When deriving a unique script key, the system starts with the receiver's
+internal key from their V2 address. This internal key represents the receiver's
+control, but using it directly for every asset would create collisions in the
+proof universe—different assets would compete for the same script key namespace.
+To prevent this, the derivation process creates a non-spendable script leaf
+containing a Pedersen commitment:
+
+```go
+// The commitment point is NUMS_key + asset_id * G
+commitmentPoint := TweakedNumsKey(assetID[:])
+leaf := txscript.NewBaseTapLeaf(
+ txscript.NewRawLeafScript(OP_CHECKSIG, commitmentPoint),
+)
+```
+
+This commitment point is mathematically unique for each asset ID. The "NUMS"
+(Nothing Up My Sleeve) key ensures that no one can actually spend using this
+script path—it's provably unspendable. The final script key emerges from
+combining the receiver's internal key with this commitment leaf:
+
+```go
+scriptPubKey := txscript.ComputeTaprootOutputKey(
+ &internalKey, leaf.TapHash(),
+)
+```
+
+The beauty of this approach is that the receiver maintains full control through
+their internal key while each asset gets its own unique script key. Hardware
+wallets that only understand miniscript policies can still sign for these
+outputs because the spending path uses the internal key directly—the commitment
+leaf is never actually executed.
+
+### The Random Alt Leaf Innovation
+
+While Pedersen commitments solve the problem of different assets colliding on
+their script keys, V2 addresses go further to ensure that even repeated
+transfers of the same asset amount create distinct on-chain outputs. This is
+where the random alternative leaf mechanism comes into play, implemented in the
+latter half of `prepareV2Outputs()`.
+
+The process is deceptively simple yet cryptographically profound. For each V2
+output, the system generates a completely random private key:
+
+```go
+randKey, err := btcec.NewPrivateKey()
+randScriptKey := asset.NewScriptKey(randKey.PubKey())
+randAltLeaf, err := asset.NewAltLeaf(randScriptKey, asset.ScriptV0)
+vOut.AltLeaves = append(vOut.AltLeaves, randAltLeaf)
+```
+
+This random leaf becomes part of the Taproot Asset commitment tree. Since the
+commitment tree's root depends on all its leaves, adding a random leaf ensures
+that the final on-chain Taproot output is unique for every transfer. The elegant
+aspect is that this randomness doesn't affect the recipient's ability to claim
+the assets—the random leaf is an alternative that's never actually used,
+existing solely to provide uniqueness.
+
+### The Complete Picture: From Address to Output
+
+Understanding how these pieces fit together requires following the complete flow
+from a V2 address to an on-chain output. The journey begins when a sender
+decodes a V2 address, extracting the receiver's internal key. This internal key
+isn't used directly; instead, it serves as the foundation for a series of
+transformations.
+
+First, the `prepareV2Outputs()` function identifies outputs destined for V2
+addresses by checking if they use send manifests (`UsesSendManifests()`). For
+each such output, it derives a unique script key using the Pedersen commitment
+method. This creates a script key that's specific to both the receiver and the
+asset being sent.
+
+Next, the random alt leaf is added to ensure transfer-level uniqueness. This
+might seem like overkill—after all, the Pedersen commitment already provides
+asset-level uniqueness—but it serves a crucial privacy function. Without it, an
+observer could potentially link multiple transfers of the same asset to the same
+V2 address by analyzing the commitment structure.
+
+Finally, when the commitment tree is built and the Taproot output is computed,
+all these elements combine. The receiver's internal key becomes the Taproot
+internal key, the Pedersen commitment and random leaves form part of the script
+tree, and the resulting Taproot output key is what appears on the Bitcoin
+blockchain.
+
+### Why This Complexity Matters
+
+The sophistication of V2's script key generation might seem excessive until you
+consider the alternatives. V0 and V1 addresses use fixed script keys, which
+means sending different assets to the same address creates identical on-chain
+outputs, destroying privacy.
+
+V2's approach solves multiple problems simultaneously. The Pedersen commitment
+ensures that each asset type gets its own namespace within a single address,
+preventing collisions. The random alt leaves ensure that each transfer is unique
+on-chain, preserving privacy even with address reuse. And the use of the
+internal key as the Taproot key means hardware wallets can still sign
+transactions without understanding the complex commitment structure.
+
+This design also enables a crucial feature: send manifests. Because V2 addresses
+generate unpredictable on-chain outputs, receivers can't simply watch the
+blockchain for incoming transfers. Instead, senders create manifests
+(`createSendManifests()` in `tapfreighter/wallet.go:383`) that inform receivers
+about the exact location and structure of their incoming assets. This
+manifest-based approach enables more complex transfer patterns, including
+grouped assets and multi-asset atomic swaps.
+
+### The Cryptographic Guarantees
+
+The security of V2's unique script key generation rests on well-established
+cryptographic foundations. The Pedersen commitment's security comes from the
+discrete logarithm problem—breaking it would require solving one of
+cryptography's fundamental hard problems. The randomness of alt leaves relies
+on the security of Bitcoin's random number generation, which has been
+battle-tested over more than a decade.
+
+Together, these mechanisms provide strong guarantees. Two different assets sent
+to the same V2 address will never collide because they'll have different
+Pedersen commitments. Two identical transfers will never create the same
+on-chain output because they'll have different random alt leaves. And throughout
+all of this, the receiver maintains complete control through their internal key,
+which can be secured in hardware wallets or complex multisig arrangements.
+
+The implementation in `prepareV2Outputs()` carefully orchestrates these
+mechanisms, ensuring that every V2 transfer benefits from these guarantees
+without requiring any special action from users. From their perspective, they
+simply provide an address and receive assets—the complexity remains hidden
+beneath an elegant interface.
+
+## Transaction Flow: From Intent to Settlement
+
+### The Complete Journey
+
+Understanding the end-to-end flow requires following an asset from user intent
+through on-chain settlement. This journey touches nearly every major component
+in the system.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant API as RPC/CLI Interface
+ participant AddrBook as Address Book
+ participant CoinSelect as Coin Selector
+ participant Freighter as Asset Wallet
+ participant VM as Taproot Asset VM
+ participant Anchor as Anchor Builder
+ participant Bitcoin as Bitcoin Network
+ participant Proof as Proof Courier
+
+ User->>API: Send 100 units to address X
+ API->>AddrBook: Decode address X
+ AddrBook->>API: Return asset ID, script key
+
+ API->>CoinSelect: Find inputs >= 100 units
+ CoinSelect->>CoinSelect: Query available UTXOs
+ CoinSelect->>Freighter: Return selected inputs
+
+ critical Virtual Transaction Construction
+ Freighter->>Freighter: Create VPacket
+ Freighter->>Freighter: Add inputs from proofs
+ Freighter->>Freighter: Create outputs (100 to X, change)
+ Freighter->>Freighter: Generate split commitment if needed
+ end
+
+ Freighter->>VM: Validate virtual transaction
+ VM->>VM: Check conservation
+ VM->>VM: Verify witnesses
+ VM->>Freighter: Validation passed
+
+ critical Passive Asset Handling
+ Freighter->>Freighter: Identify passive assets
+ Freighter->>Freighter: Create re-anchor packets
+ Freighter->>Freighter: Sign passive transfers
+ end
+
+ Freighter->>Anchor: Create anchor transaction
+ Anchor->>Anchor: Build commitment trees
+ Anchor->>Anchor: Create Taproot outputs
+ Anchor->>Bitcoin: Fund and sign PSBT
+
+ Bitcoin->>Bitcoin: Broadcast transaction
+ Bitcoin->>Freighter: Transaction confirmed
+
+ Freighter->>Proof: Generate transfer proof
+ Proof->>Proof: Send to recipient
+```
+
+### Phase 1: Intent and Preparation
+
+The flow begins when a user expresses intent to transfer assets. The address
+decoding (`address/address.go`) extracts the recipient's script key and asset
+requirements. For V2 addresses, this includes proof courier information for
+later delivery.
+
+Coin selection (`tapfreighter/coin_select.go`) then identifies suitable inputs.
+This isn't simple UTXO selection—we must consider asset types, amounts, and
+whether assets are in groups. The selector returns `AnchoredCommitment`
+structures containing both the assets and their Bitcoin anchor information.
+
+### Phase 2: Virtual Transaction Construction
+
+The virtual packet creation (`tappsbt/create.go:FromAddresses()`) establishes
+the transaction structure. For address sends, we know we need change outputs
+(non-interactive), so the function pre-allocates space. The funding process
+(`tapfreighter/wallet.go:FundPacket()`) then populates inputs and calculates
+change amounts.
+
+If the transfer requires asset division, split commitment creation occurs here.
+The split logic must carefully track which output becomes the "root" (contains
+the split commitment root) versus split outputs (contain proofs).
+
+### Phase 3: Commitment and Anchoring
+
+With virtual transactions prepared, commitment tree construction begins
+(`tapsend/send.go:CreateOutputCommitments()`). This process builds MS-SMT
+structures for each anchor output, carefully avoiding key collisions and
+maintaining proof structures.
+
+The anchor transaction template (`tapsend/send.go:CreateAnchorTx()`) initially
+contains dummy outputs. The real commitment data is inserted by
+`UpdateTaprootOutputKeys()`, which replaces dummy values with actual Taproot
+commitment scripts.
+
+### Phase 4: Settlement and Proof Distribution
+
+After Bitcoin confirmation, proof generation creates the evidence recipients
+need to claim their assets. The proof includes the Bitcoin transaction, the
+Merkle proof linking the asset to the Taproot output, and the complete history
+back to genesis.
+
+## Implementation Reference
+
+### Critical Files and Their Responsibilities
+
+#### Core Asset Logic
+- `asset/asset.go`: Asset structure definitions, including split commitment
+ fields
+- `asset/witness.go`: Witness structures and validation, including
+ `IsSplitCommitWitness()`
+- `asset/tx.go`: Virtual transaction primitives
+
+#### Transfer Orchestration
+- `tapfreighter/wallet.go`: Main wallet operations, including
+ `createPassivePacket()` (bug #1 location)
+- `tapfreighter/coin_select.go`: Asset input selection
+- `tapsend/send.go`: High-level transfer coordination
+- `tapsend/allocation.go`: Output allocation and tombstone logic
+
+#### Commitment Management
+- `commitment/tap.go`: Taproot Asset commitment construction
+- `commitment/split.go`: Split commitment creation and validation
+- `mssmt/`: Merkle Sum Sparse Merkle Tree implementation
+
+#### Storage Layer
+- `tapdb/assets_common.go`: Asset storage operations, including
+ `upsertAssetsWithGenesis()` (bug #2 location)
+- `tapdb/assets_store.go`: Asset queries and updates
+- `proof/proof.go`: Proof construction and verification
+
+#### Virtual Transaction Layer
+- `tappsbt/interface.go`: Virtual PSBT definitions
+- `tappsbt/create.go`: Virtual packet construction, including V2 address
+ handling
+- `vm/vm.go`: Taproot Asset VM for validation
+
+### Key Functions to Understand
+
+1. **`FundAddressSend()`** (`tapfreighter/wallet.go:251`): Entry point for
+ address-based transfers
+2. **`createPassivePacket()`** (`tapfreighter/wallet.go:489`): Passive asset
+ re-anchoring
+3. **`NewSplitCommitment()`** (`commitment/split.go:142`): Split creation
+4. **`upsertAssetsWithGenesis()`** (`tapdb/assets_common.go:209`): Asset storage
+ with genesis
+5. **`CreateOutputCommitments()`** (`tapsend/send.go:943`): Commitment tree
+ construction
+6. **`validateSplit()`** (`vm/vm.go:695`): Split commitment validation
+
+### Testing Strategy
+
+Comprehensive testing must cover:
+
+1. **State Transitions**: Test assets moving through all possible states
+2. **Cross-Node Scenarios**: Import/export with full state preservation
+3. **Split Lifecycles**: Create, transfer, and spend split assets
+4. **Passive Asset Scenarios**: Multiple assets in single UTXOs
+5. **Address Versions**: V0, V1, and V2 address handling
+
+Example test pattern:
+```go
+// Test split asset becoming passive then imported
+func TestSplitPassiveImport(t *testing.T) {
+ // 1. Create and split asset
+ asset := mintAsset(t, 100)
+ splits := splitAsset(t, asset, []uint64{60, 40})
+
+ // 2. Transfer one split, making other passive
+ transferAsset(t, splits[0])
+ assertPassive(t, splits[1])
+
+ // 3. Export and import on new node
+ proof := exportProof(t, splits[1])
+ imported := importProof(t, proof)
+
+ // 4. Verify split commitment preserved
+ require.NotNil(t, imported.PrevWitnesses[0].SplitCommitment)
+
+ // 5. Ensure imported asset is spendable
+ transferAsset(t, imported)
+}
+```
diff --git a/docs/hardware-wallet-support.md b/docs/hardware-wallet-support.md
new file mode 100644
index 000000000..37427b33e
--- /dev/null
+++ b/docs/hardware-wallet-support.md
@@ -0,0 +1,116 @@
+# How to add Hardware Wallet support
+
+With `v0.7.0`, `tapd` uses four different forms of scripts/tweaks for anchoring
+commitment roots into Bitcoin transactions that can be relevant for hardware
+signing devices:
+ - The "Taproot Asset Root Commitment": The pseudo-leaf that's placed in the
+ Taproot merkle tree to commit to asset mints and transfers.
+ - Relevant when signing any asset mint or transfer transaction. The Taproot
+ internal key used for the mint or transfer output(s) would be a key that
+ belongs to the signing device. And the Taproot Asset Root Commitment pseudo
+ leaf would be the single leaf in the tree (unless there are more
+ user-defined script leaves added, as would be the case for Lightning
+ Channel outputs)
+ - The V0 group key scheme that tweaks the group internal key twice.
+ - Deprecated, should not be used when targeting Hardware Wallet support. The
+ commitment is a simple double tweak to arrive at the final ("tweaked")
+ group key:
+ ```go
+ // internalKey = rawKey + singleTweak * G
+ // tweakedGroupKey = TapTweak(internalKey, tapscriptRoot)
+ ```
+ Where `singleTweak` is the asset ID of the group anchor and the
+ `tapscriptRoot` is either an empty byte slice or the root of a custom
+ script tapscript tree.
+ - The `OP_RETURN` commitment scheme for signing minting events with the group
+ key.
+ - Currently only relevant as an option to choose from when defining a group
+ key V1. Relevant for signing new tranches of assets only: A single
+ `OP_RETURN` leaf would be present in the group key's tapscript tree that
+ commits to the group anchor's asset ID.
+ - The Pedersen commitment scheme for signing minting events with the group key
+ and for generating unique script keys in V2 TAP address sends.
+ - Relevant as an option to choose from when defining a group
+ key V1. Relevant for signing new tranches of assets: A single
+ ` OP_CHECKSIG` leaf would be present in the group key's
+ tapscript tree that commits to the group anchor's asset ID through a
+ Pedersen commitment key.
+ - This is also relevant for outputs created for sends to V2 TAP addresses.
+ The receiver script keys are constructed with a Pedersen commitment, so
+ if the internal key of the script key is held in a signing device, then
+ authorizing the spend of such an output would require the signing device
+ to be able to deal with such a leaf being present.
+
+## On-chain Taproot Asset Root Commitment Structure
+
+The Taproot Asset commitment is what is placed in a tap leaf of a transaction
+output's tapscript tree. The exact structure of the leaf script depends on the
+commitment version.
+
+### V0 and V1 Commitments
+
+For `TapCommitmentV0` and `TapCommitmentV1`, the tap leaf script is constructed
+as follows:
+
+`version (1 byte) || TaprootAssetsMarker (32 bytes) || root_hash (32 bytes) ||
+root_sum (8 bytes)`
+
+Where:
+- `version`: The `TapCommitmentVersion`, which is `0` for V0 and `1` for V1.
+- `TaprootAssetsMarker`: A static marker to identify the leaf as a Taproot Asset
+ commitment. It is the `sha256` hash of the string `taproot-assets`.
+- `root_hash`: The MS-SMT root of the `TapCommitment`, which commits to all the
+ asset commitments within it.
+- `root_sum`: The sum of all asset amounts under that `TapCommitment` root.
+
+### V2 Commitments
+
+For `TapCommitmentV2`, the tap leaf script is constructed as follows:
+
+`tag (32 bytes) || version (1 byte) || root_hash (32 bytes) ||
+root_sum (8 bytes)`
+
+Where:
+- `tag`: A tagged hash to uniquely identify this as a V2+ Taproot Asset
+ commitment. It is the `sha256` hash of the string `taproot-assets:194243`.
+- `version`: The `TapCommitmentVersion`, which is `2` for V2.
+- `root_hash`: The MS-SMT root of the `TapCommitment`, which commits to all the
+ asset commitments within it.
+- `root_sum`: The sum of all asset amounts under that `TapCommitment` root.
+
+## Group Key Commitment Schemes
+
+To commit to a group key, two main schemes are used to create non-spendable
+tapscript leaves. These leaves are used to commit to the genesis asset ID within
+the group key's tapscript tree and therefore a signing device signing a new
+asset tranche needs to be able to deal with such a leaf being present in the
+tapscript tree.
+
+### OP_RETURN Commitment
+
+This scheme creates a non-spendable script by using the `OP_RETURN` opcode.
+The script is constructed as follows:
+
+`OP_RETURN || `
+
+Where:
+- `data`: The data to be committed to, which is typically the genesis asset ID.
+
+This creates a script that will terminate execution early, making it provably
+unspendable.
+
+### Pedersen Commitment
+
+This scheme uses a normal `OP_CHECKSIG` operator with a public key that cannot
+be signed for. This special public key is generated using a Pedersen
+commitment. The script is constructed as follows:
+
+` OP_CHECKSIG`
+
+Where:
+- `tweaked_nums_key`: A public key derived from a Pedersen commitment. The
+ message for the commitment is the asset ID (or 32 zero bytes if no data is
+ provided). To achieve hardware wallet support, this key is turned into an
+ extended key (xpub), and a child key at path `0/0` is used as the actual
+ public key that goes into the `OP_CHECKSIG` script.
+
diff --git a/docs/taproot-asset-channels.md b/docs/taproot-asset-channels.md
new file mode 100644
index 000000000..4451f05d9
--- /dev/null
+++ b/docs/taproot-asset-channels.md
@@ -0,0 +1,165 @@
+# Taproot Asset Channels
+
+This document provides a technical overview of how Taproot Asset Channels are implemented, leveraging LND's extensibility features to support asset transfers within Lightning Channels without requiring LND to have native knowledge of Taproot Assets.
+
+## How LND Handles Custom Channels
+
+The core principle behind Taproot Asset Channels is LND's ability to manage "custom channels." LND itself does not understand the concept of Taproot Assets. Instead, it provides a set of hooks and delegates the logic for handling custom channel types to an external process. In this case, that external process is `tapd`.
+
+LND manages the BTC layer of the channel (funding, commitments, HTLCs) as it normally would. However, for channels that are designated as "custom," LND will:
+1. Store opaque data blobs associated with the channel, commitments, and HTLCs.
+2. Call out to the external process (`tapd`) at critical stages of the channel lifecycle (funding, state updates, closing, etc.) via a set of gRPC-based hooks.
+
+`tapd` is responsible for managing the asset layer, providing LND with the necessary information to correctly construct and sign transactions that also commit to the state of the assets in the channel.
+
+## On-Chain Footprint
+
+A Taproot Asset Channel modifies the standard Lightning channel structure by adding an additional leaf to the tapscript tree of the funding output.
+
+A standard Taproot channel's funding output is a 2-of-2 multisig key, represented as a Taproot output. The script path for this output might contain scripts for revocation and CSV-delayed spends.
+
+For a Taproot Asset Channel, an additional leaf is added to this tree: the **Taproot Asset Commitment**.
+
+
+
+This new leaf (`hC` in the diagram) contains a commitment to the assets held within the channel, effectively anchoring the asset state to the on-chain funding transaction.
+
+## Off-Chain Data: The "Blob" Architecture
+
+To manage the state of assets off-chain, `tapd` provides LND with opaque data blobs that are stored at different levels of the channel structure. LND does not interpret these blobs; it simply stores them and provides them back to `tapd` when invoking the hooks.
+
+
+
+There are three main types of blobs:
+
+### 1. `OpenChannel` Blob (Channel Level)
+
+This blob is associated with the channel as a whole and is created during the funding process. It contains the initial state of the assets in the channel.
+
+```go
+// OpenChannel is a record that represents the capacity information related to
+// a commitment. This entails all the (asset_id, amount, proof) tuples and other
+// information that we may need to be able to sign the TAP portion of the
+// commitment transaction.
+type OpenChannel struct {
+ // FundedAssets is a list of asset outputs that was committed to the
+ // funding output of a commitment.
+ FundedAssets tlv.RecordT[tlv.TlvType0, AssetOutputListRecord]
+
+ // DecimalDisplay is the asset's unit precision.
+ DecimalDisplay tlv.RecordT[tlv.TlvType1, uint8]
+
+ // GroupKey is the optional group key used to fund this channel.
+ GroupKey tlv.OptionalRecordT[tlv.TlvType2, *btcec.PublicKey]
+}
+```
+
+### 2. `Commitment` Blob (Commitment Level)
+
+For each commitment transaction in the channel, a `Commitment` blob is stored. This blob represents the state of the assets for that specific commitment.
+
+```go
+// Commitment is a record that represents the current state of a commitment.
+// This entails all the (asset_id, amount, proof) tuples and other information
+// that we may need to be able to sign the TAP portion of the commitment
+// transaction.
+type Commitment struct {
+ // LocalAssets is a list of all asset outputs that represent the current
+ // local asset balance of the commitment.
+ LocalAssets tlv.RecordT[tlv.TlvType0, AssetOutputListRecord]
+
+ // RemoteAssets is a list of all asset outputs that represents the
+ // current remote asset balance of the commitment.
+ RemoteAssets tlv.RecordT[tlv.TlvType1, AssetOutputListRecord]
+
+ // OutgoingHtlcAssets is a list of all outgoing in-flight HTLCs and the
+ // asset balance change that they represent.
+ OutgoingHtlcAssets tlv.RecordT[tlv.TlvType2, HtlcAssetOutput]
+
+ // IncomingHtlcAssets is a list of all incoming in-flight HTLCs and the
+ // asset balance change that they represent.
+ IncomingHtlcAssets tlv.RecordT[tlv.TlvType3, HtlcAssetOutput]
+
+ // AuxLeaves are the auxiliary leaves that correspond to the commitment.
+ AuxLeaves tlv.RecordT[tlv.TlvType4, AuxLeaves]
+}
+```
+
+### 3. `Htlc` Blob (HTLC Level)
+
+Each HTLC within a commitment can have its own blob. For Taproot Asset Channels, this is used to carry the asset information for that specific HTLC.
+
+```go
+// Htlc is a record that represents the capacity change related to an in-flight
+// HTLC. This entails all the (asset_id, amount) tuples and other information
+// that we may need to be able to update the TAP portion of a commitment
+// balance.
+type Htlc struct {
+ // Amounts is a list of asset balances that are changed by the HTLC.
+ Amounts tlv.RecordT[HtlcAmountRecordType, AssetBalanceListRecord]
+
+ // RfqID is the RFQ ID that corresponds to the HTLC.
+ RfqID tlv.OptionalRecordT[HtlcRfqIDType, ID]
+}
+```
+
+## LND Hooks for Custom Logic
+
+`tapd` implements a series of interfaces defined by LND to inject asset-specific logic into the channel management process. These hooks are collectively managed through a set of "Auxiliary" components.
+
+Here is a summary of the key hooks and their functions:
+
+- **`AuxFundingController`**: Manages the asset-specific parts of the channel funding process. It handles custom messages exchanged between peers to agree on the assets being committed to the channel.
+- **`AuxLeafCreator`**: Responsible for creating the auxiliary tapscript leaves that commit to the asset state. When LND builds a commitment transaction, it calls this hook to get the asset commitment leaf.
+- **`AuxLeafSigner`**: Signs the asset-related parts of the commitment transaction. For HTLCs, this involves signing second-level transactions that are part of the HTLC scripts.
+- **`AuxTrafficShaper`**: Controls how payments are routed. For asset channels, it ensures that an HTLC carrying assets is only forwarded over channels that also contain assets. It also calculates the available asset bandwidth.
+- **`InvoiceHtlcModifier`**: Intercepts incoming HTLCs for an invoice. For asset invoices, it verifies the incoming asset amount and modifies the BTC amount of the HTLC to reflect the agreed-upon exchange rate.
+- **`AuxChanCloser`**: Handles the cooperative closure of a channel, ensuring that the assets are correctly distributed in the closing transaction.
+- **`AuxSweeper` / `AuxContractResolver`**: Manages the process of sweeping on-chain funds from a force-closed channel. This includes handling the outputs from commitment transactions and second-level HTLC transactions to recover assets.
+
+Anything `lnd` needs from an aux component it will get through a call through
+the code hooks. Anything `tapd` needs from `lnd`, it gets normally through a
+gRPC call:
+
+
+
+## Channel Funding Sequence
+
+The funding of a Taproot Asset Channel involves a custom message flow between the two `tapd` instances, orchestrated by LND.
+
+
+
+1. **Initiation**: The process starts with a `fundchannel` command.
+2. **Proof Exchange**: The initiator's `tapd` sends `SendCustomMessage` calls to its LND node, containing proofs of the assets to be funded. LND forwards these as `CustomMessage`s to the peer.
+3. **Funding Ack**: The responder's `tapd` verifies the proofs and sends back a funding acknowledgment, again via custom messages.
+4. **LND Funding Flow**: Once the asset states are agreed upon, the standard LND channel funding flow (`OpenChannel`, `AcceptChannel`, etc.) proceeds. `tapd`'s `AuxFundingController` provides LND with the necessary asset commitment information.
+5. **Asset Funding Created**: After the BTC funding transaction is created, the initiator's `tapd` sends a final custom message with the proofs of the assets in their final state within the channel.
+6. **Finalization**: The channel is finalized with `FundingCreated` and `FundingSigned` messages, and the funding transaction is broadcast.
+
+## Asset Payment Flow
+
+Sending an asset payment across the network also involves the LND hooks. The following diagram illustrates a multi-hop payment from Alice to Dave, through Bob and Charlie.
+
+
+
+Here's a step-by-step breakdown:
+
+1. **Alice (Payer)**:
+ * Alice wants to pay an invoice from Dave. The invoice is for a certain amount of "beefbuxx".
+ * Her `tapd`'s **`TrafficShaper`** is invoked. It determines the outgoing channel, generates the HTLC blob containing the asset information (`beefbuxx: 10`), and overwrites the BTC amount of the HTLC to a minimal on-chain value (e.g., 550 sat).
+
+2. **Bob (Hop)**:
+ * Bob's LND node receives the HTLC. Since it has custom records, it invokes the hooks.
+ * The **`HtlcInterceptor`** (a general LND concept, implemented by `tapd` for assets) is triggered.
+ * Bob's `tapd` inspects the incoming HTLC blob. In this example, it checks an RFQ ID and amount, and then overwrites the outgoing HTLC's BTC amount to a new value (e.g., 50001 sat) for the next hop. The asset blob is forwarded.
+
+3. **Charlie (Hop)**:
+ * Charlie's LND node also invokes the **`HtlcInterceptor`**.
+ * His `tapd` checks the incoming short channel ID (SCID) and amount, generates a new HTLC blob for the next hop, and again overwrites the BTC amount.
+
+4. **Dave (Payee)**:
+ * Dave's LND node receives the final HTLC.
+ * The **`InvoiceHtlcModifier`** hook is called.
+ * Dave's `tapd` inspects the HTLC blob, verifies that it contains the correct asset and amount (`beefbuxx: 10`), and then modifies the BTC amount of the HTLC to match the full value of the invoice (e.g., 50k sats). This allows the invoice to be settled in LND's accounting system.
+
+This flow allows assets to be tunneled through the Lightning Network, with `tapd` instances at each hop managing the asset state transitions, while LND handles the underlying BTC-level HTLC mechanics.
diff --git a/docs/taproot-asset-channels/litd-hooks.png b/docs/taproot-asset-channels/litd-hooks.png
new file mode 100644
index 000000000..ed54d2b92
Binary files /dev/null and b/docs/taproot-asset-channels/litd-hooks.png differ
diff --git a/docs/taproot-asset-channels/tap-channel-funding.png b/docs/taproot-asset-channels/tap-channel-funding.png
new file mode 100644
index 000000000..86a166acc
Binary files /dev/null and b/docs/taproot-asset-channels/tap-channel-funding.png differ
diff --git a/docs/taproot-asset-channels/tap-channel-payment.drawio b/docs/taproot-asset-channels/tap-channel-payment.drawio
new file mode 100644
index 000000000..69858dda3
--- /dev/null
+++ b/docs/taproot-asset-channels/tap-channel-payment.drawio
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/taproot-asset-channels/tap-channel-payment.png b/docs/taproot-asset-channels/tap-channel-payment.png
new file mode 100644
index 000000000..4f982a069
Binary files /dev/null and b/docs/taproot-asset-channels/tap-channel-payment.png differ
diff --git a/docs/taproot-asset-channels/taproot-asset-channels.png b/docs/taproot-asset-channels/taproot-asset-channels.png
new file mode 100644
index 000000000..7a7dbad6b
Binary files /dev/null and b/docs/taproot-asset-channels/taproot-asset-channels.png differ
diff --git a/docs/taproot-asset-channels/taproot-chans-to-local.png b/docs/taproot-asset-channels/taproot-chans-to-local.png
new file mode 100644
index 000000000..b95faddcb
Binary files /dev/null and b/docs/taproot-asset-channels/taproot-chans-to-local.png differ
diff --git a/tapgarden/README.md b/tapgarden/README.md
new file mode 100644
index 000000000..59cb0abbd
--- /dev/null
+++ b/tapgarden/README.md
@@ -0,0 +1,175 @@
+# Asset Custodian
+
+The `Custodian` is a core component within the `tapgarden` package responsible
+for managing the lifecycle of incoming Taproot Assets. It operates as an
+event-driven system that listens for various triggers, tracks the state of each
+incoming asset transfer, and ensures that assets are securely and verifiably
+received into the wallet.
+
+## Core Responsibilities
+
+- **Address Management**: Imports new addresses into the underlying `lnd`
+ wallet for on-chain detection (for V0/V1 addresses) or subscribes to an
+ authentication mailbox for message-based notifications from the sender (for
+ V2+ addresses).
+- **On-Chain Event Monitoring**: Watches for new transactions that match managed
+ Taproot output keys.
+- **Proof Retrieval**: Coordinates with `ProofCourier` services to fetch asset
+ provenance proofs from senders.
+- **Proof Validation & Storage**: Verifies the integrity and validity of
+ incoming proofs and stores them in the `ProofArchive`.
+- **State Management**: Tracks the status of each inbound asset transfer through
+ a series of states, from detection to final completion.
+
+## Event Triggers and State Flow
+
+The Custodian's logic is driven by four primary event triggers. Each trigger
+initiates a series of actions that advance an asset transfer through its
+lifecycle.
+
+### Asset State Transitions
+
+The lifecycle of an incoming asset transfer is tracked by the `address.Status`
+field of an `address.Event`. The diagram below illustrates the possible state
+transitions.
+
+```mermaid
+stateDiagram-v2
+ direction LR
+ [*] --> CREATED: New Address
+ CREATED --> DETECTED: On-chain TX detected (V0/V1)
+ CREATED --> CONFIRMED: Mailbox message received (V2)
+ DETECTED --> CONFIRMED: On-chain TX confirmed
+ CONFIRMED --> PROOF_RECEIVED: Proof retrieval successful
+ PROOF_RECEIVED --> COMPLETED: All proofs for transfer validated
+ COMPLETED --> [*]
+
+ state "Event States" as States {
+ DETECTED: StatusTransactionDetected
+ CONFIRMED: StatusTransactionConfirmed
+ PROOF_RECEIVED: StatusProofReceived
+ COMPLETED: StatusCompleted
+ }
+```
+
+---
+
+### Trigger 1: New Address Creation
+
+When a new Taproot Asset address is created and added to the `AddrBook`, the
+Custodian is notified. Its behavior depends on the address version.
+
+- **V0/V1 Addresses**: The Custodian takes the `TaprootOutputKey` from the
+ address and imports it into the `lnd` wallet. This allows the wallet to scan
+ the blockchain for transactions paying to this specific key.
+- **V2+ Addresses**: The Custodian subscribes to a `ProofCourier` of type
+ `AuthMailbox` using the address's unique script key. It does not import any
+ keys into the on-chain wallet, as V2 transfers are communicated as separate
+ messages sent through the authenticated mailbox system.
+
+### Trigger 2: New Wallet Transaction (V0/V1 Addresses)
+
+When the `WalletAnchor` detects a new transaction, the Custodian inspects it.
+
+1. It checks if the transaction has a Taproot output that belongs to the
+ internal wallet.
+2. If a match is found, it queries the `AddrBook` to see if this output key
+ corresponds to a known Taproot Asset address.
+3. If it's a match, a new `address.Event` is created with the status
+ `StatusTransactionDetected`.
+4. When the transaction receives its first confirmation, the event's status is
+ updated to `StatusTransactionConfirmed`. This triggers the proof retrieval
+ process.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant AddrBook
+ participant Custodian
+ participant WalletAnchor
+ participant ProofCourier
+ participant ProofArchive
+
+ User->>AddrBook: Create New Address (V0/V1)
+ AddrBook->>Custodian: Notify(New Address)
+ Custodian->>WalletAnchor: ImportTaprootOutput(key)
+ note right of Custodian: Custodian now watches for on-chain TXs
+
+ WalletAnchor->>Custodian: Notify(New TX)
+ Custodian->>AddrBook: GetOrCreateEvent(StatusTransactionDetected)
+ WalletAnchor->>Custodian: Notify(TX Confirmed)
+ Custodian->>AddrBook: UpdateEvent(StatusTransactionConfirmed)
+ activate Custodian
+ Custodian->>ProofCourier: ReceiveProof()
+ ProofCourier-->>Custodian: Return Proof
+ deactivate Custodian
+ Custodian->>ProofArchive: ImportProofs()
+ ProofArchive->>Custodian: Notify(New Proof)
+ Custodian->>AddrBook: UpdateEvent(StatusProofReceived)
+ Custodian->>AddrBook: CompleteEvent(StatusCompleted)
+```
+
+### Trigger 3: New Mailbox Message (V2+ Addresses)
+
+For V2 addresses, the process is initiated by an incoming authenticated mailbox
+message.
+
+1. The Custodian receives an encrypted message from the `AuthMailbox` server
+ it's subscribed to.
+2. It decrypts the message using the address's script key to reveal a
+ `SendFragment`. This fragment contains the outpoint of the on-chain anchor
+ transaction.
+3. Using the information from the fragment, the Custodian creates a new
+ `address.Event` directly with the status `StatusTransactionConfirmed`, as the
+ fragment is only sent after the transaction is confirmed.
+4. This immediately triggers the proof retrieval process.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant AddrBook
+ participant Custodian
+ participant MailboxServer
+ participant ProofCourier
+ participant ProofArchive
+
+ User->>AddrBook: Create New Address (V2)
+ AddrBook->>Custodian: Notify(New Address)
+ Custodian->>MailboxServer: Subscribe(script_key)
+ note right of Custodian: Custodian now listens for mailbox messages
+
+ MailboxServer->>Custodian: Notify(New Message)
+ activate Custodian
+ Custodian->>Custodian: Decrypt Message (SendFragment)
+ Custodian->>AddrBook: GetOrCreateEvent(StatusTransactionConfirmed)
+ deactivate Custodian
+
+ activate Custodian
+ Custodian->>ProofCourier: ReceiveProof()
+ ProofCourier-->>Custodian: Return Proof
+ deactivate Custodian
+ Custodian->>ProofArchive: ImportProofs()
+ ProofArchive->>Custodian: Notify(New Proof)
+ Custodian->>AddrBook: UpdateEvent(StatusProofReceived)
+ Custodian->>AddrBook: CompleteEvent(StatusCompleted)
+```
+
+### Trigger 4: New Proof Received
+
+This trigger is the final step in the process, common to all address versions.
+
+1. After a transaction is confirmed (either on-chain or via mailbox), the
+ `receiveProofs` function is called. It uses the `ProofCourier` specified in
+ the address to fetch the asset proof.
+2. The fetched proof is sent to the `ProofArchive` for validation and storage.
+3. Importing a proof into the `ProofArchive` will make sure it is imported into
+ both the file-based local proof archive and the database-backed proof store.
+ Both those stores will trigger a notification (via the `ProofNotifier`) for
+ received proofs, so the below might be invoked multiple times
+4. The Custodian finds the corresponding `address.Event` and updates its status
+ to `StatusProofReceived`.
+5. It then calls `setReceiveCompleted`, which verifies that all required proofs
+ for the transfer have been received.
+6. Finally, the event status is set to `StatusCompleted`, marking the end of the
+ asset custody process. The asset is now considered fully received and
+ settled.