diff --git a/itest/assertions.go b/itest/assertions.go index 219c18ab1..9bb51fc81 100644 --- a/itest/assertions.go +++ b/itest/assertions.go @@ -17,6 +17,7 @@ import ( "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/taprpc" @@ -1562,6 +1563,30 @@ func AssertAssetsMinted(t *testing.T, return assetList } +func AssertGenesisOutput(t *testing.T, output *taprpc.ManagedUtxo, + sibling commitment.TapscriptPreimage) { + + // Fetch the encoded tapscript sibling from an anchored asset, and check + // it against the expected sibling. + require.True(t, len(output.Assets) > 1) + rpcSibling := output.Assets[0].ChainAnchor.TapscriptSibling + require.True(t, fn.All(output.Assets, func(a *taprpc.Asset) bool { + return bytes.Equal(a.ChainAnchor.TapscriptSibling, rpcSibling) + })) + encodedSibling, siblingHash, err := commitment. + MaybeEncodeTapscriptPreimage(&sibling) + require.NoError(t, err) + require.Equal(t, encodedSibling, rpcSibling) + + // We should be able to recompute a merkle root from the tapscript + // sibling hash and the Taproot Asset Commitment root that matches what + // is stored in the managed output. + expectedMerkleRoot := asset.NewTapBranchHash( + (chainhash.Hash)(output.TaprootAssetRoot), *siblingHash, + ) + require.Equal(t, expectedMerkleRoot[:], output.MerkleRoot) +} + func AssertAssetBalances(t *testing.T, client taprpc.TaprootAssetsClient, simpleAssets, issuableAssets []*taprpc.Asset) { diff --git a/itest/assets_test.go b/itest/assets_test.go index 556c1c142..644f328dc 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -1,19 +1,28 @@ package itest import ( + "bytes" "context" "crypto/tls" "net/http" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/test" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" "golang.org/x/net/http2" ) @@ -334,3 +343,130 @@ func testMintAssetNameCollisionError(t *harnessTest) { collideAssetName := rpcCollideAsset[0].AssetGenesis.Name require.Equal(t.t, commonAssetName, collideAssetName) } + +// testMintAssetsWithTapscriptSibling tests that a batch of assets can be minted +// with a tapscript sibling, and that the genesis output from that mint can be +// spend via the script path. +func testMintAssetsWithTapscriptSibling(t *harnessTest) { + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout) + defer cancel() + + // Build the tapscript tree. + sigLockPrivKey := test.RandPrivKey(t.t) + hashLockPreimage := []byte("foobar") + hashLockLeaf := test.ScriptHashLock(t.t, hashLockPreimage) + sigLeaf := test.ScriptSchnorrSig(t.t, sigLockPrivKey.PubKey()) + siblingTree := txscript.AssembleTaprootScriptTree(hashLockLeaf, sigLeaf) + + siblingBranch := txscript.NewTapBranch( + siblingTree.RootNode.Left(), siblingTree.RootNode.Right(), + ) + siblingPreimage := commitment.NewPreimageFromBranch(siblingBranch) + typedBranch := asset.TapTreeNodesFromBranch(siblingBranch) + rawBranch := fn.MapOptionZ(asset.GetBranch(typedBranch), asset.ToBranch) + require.Len(t.t, rawBranch, 2) + siblingReq := mintrpc.FinalizeBatchRequest_Branch{ + Branch: &taprpc.TapBranch{ + LeftTaphash: rawBranch[0], + RightTaphash: rawBranch[1], + }, + } + + rpcSimpleAssets := MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, t.tapd, simpleAssets, + WithSiblingBranch(siblingReq), + ) + rpcIssuableAssets := MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, t.tapd, issuableAssets, + ) + + AssertAssetBalances(t.t, t.tapd, rpcSimpleAssets, rpcIssuableAssets) + + // Filter the managed UTXOs to select the genesis UTXO with the + // tapscript sibling. + utxos, err := t.tapd.ListUtxos(ctxt, &taprpc.ListUtxosRequest{}) + require.NoError(t.t, err) + + utxoWithTapSibling := func(utxo *taprpc.ManagedUtxo) bool { + return !bytes.Equal(utxo.TaprootAssetRoot, utxo.MerkleRoot) + } + mintingOutputWithSibling := fn.Filter( + maps.Values(utxos.ManagedUtxos), utxoWithTapSibling, + ) + require.Len(t.t, mintingOutputWithSibling, 1) + genesisWithSibling := mintingOutputWithSibling[0] + + // Verify that all assets anchored in the output with the tapscript + // sibling have the correct sibling preimage. Also verify that the final + // tweak used for the genesis output is derived from the tapscript + // sibling created above and the batch Taproot Asset commitment. + AssertGenesisOutput(t.t, genesisWithSibling, siblingPreimage) + + // Extract the fields needed to construct a script path spend, which + // includes the Taproot Asset commitment root, the final tap tweak, and + // the internal key. + mintTapTweak := genesisWithSibling.MerkleRoot + mintTapTreeRoot := genesisWithSibling.TaprootAssetRoot + mintInternalKey, err := btcec.ParsePubKey( + genesisWithSibling.InternalKey, + ) + require.NoError(t.t, err) + + mintOutputKey := txscript.ComputeTaprootOutputKey( + mintInternalKey, mintTapTweak, + ) + mintOutputKeyIsOdd := mintOutputKey.SerializeCompressed()[0] == 0x03 + siblingScriptHash := sigLeaf.TapHash() + + // Build the control block and witness. + inclusionProof := bytes.Join( + [][]byte{siblingScriptHash[:], mintTapTreeRoot}, nil, + ) + hashLockControlBlock := txscript.ControlBlock{ + InternalKey: mintInternalKey, + OutputKeyYIsOdd: mintOutputKeyIsOdd, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: inclusionProof, + } + hashLockControlBlockBytes, err := hashLockControlBlock.ToBytes() + require.NoError(t.t, err) + + hashLockWitness := wire.TxWitness{ + hashLockPreimage, hashLockLeaf.Script, hashLockControlBlockBytes, + } + + // Make a non-tap output from Bob to use in a TX spending Alice's + // genesis UTXO. + burnOutput := MakeOutput( + t, t.lndHarness.Bob, lnrpc.AddressType_TAPROOT_PUBKEY, 500, + ) + + // Construct and publish the TX. + genesisOutpoint, err := wire.NewOutPointFromString( + genesisWithSibling.OutPoint, + ) + require.NoError(t.t, err) + + burnTx := wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: *genesisOutpoint, + Witness: hashLockWitness, + }}, + TxOut: []*wire.TxOut{burnOutput}, + } + + var burnTxBuf bytes.Buffer + require.NoError(t.t, burnTx.Serialize(&burnTxBuf)) + t.lndHarness.Bob.RPC.PublishTransaction(&walletrpc.Transaction{ + TxHex: burnTxBuf.Bytes(), + }) + + // Bob should detect the TX, and the resulting confirmed UTXO once + // a new block is mined. + t.lndHarness.Miner.AssertNumTxsInMempool(1) + t.lndHarness.AssertNumUTXOsUnconfirmed(t.lndHarness.Bob, 1) + t.lndHarness.MineBlocksAndAssertNumTxes(1, 1) + t.lndHarness.AssertNumUTXOsWithConf(t.lndHarness.Bob, 1, 1, 1) +} diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index 26859c095..e5bbe67ae 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -17,6 +17,10 @@ var testCases = []*testCase{ name: "asset name collision raises mint error", test: testMintAssetNameCollisionError, }, + { + name: "mint assets with tap sibling", + test: testMintAssetsWithTapscriptSibling, + }, { name: "addresses", test: testAddresses, diff --git a/itest/utils.go b/itest/utils.go index 8cf11cbed..1e2c2e865 100644 --- a/itest/utils.go +++ b/itest/utils.go @@ -9,7 +9,6 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" @@ -130,6 +129,23 @@ type UTXORequest struct { Amount int64 } +// MakeOutput creates a new TXO from a given output type and amount. +func MakeOutput(t *harnessTest, wallet *node.HarnessNode, + addrType lnrpc.AddressType, amount int64) *wire.TxOut { + + addrResp := wallet.RPC.NewAddress(&lnrpc.NewAddressRequest{ + Type: addrType, + }) + addr, err := btcutil.DecodeAddress( + addrResp.Address, harnessNetParams, + ) + require.NoError(t.t, err) + + addrScript := t.lndHarness.PayToAddrScript(addr) + + return wire.NewTxOut(amount, addrScript) +} + // SetNodeUTXOs sets the wallet state for the given node wallet to a set of // UTXOs of a specific type and value. func SetNodeUTXOs(t *harnessTest, wallet *node.HarnessNode, @@ -146,28 +162,9 @@ func SetNodeUTXOs(t *harnessTest, wallet *node.HarnessNode, // Build TXOs from the UTXO requests, which will be used by the miner // to build a TX. - makeOutputs := func(req *UTXORequest) *wire.TxOut { - addrResp := wallet.RPC.NewAddress( - &lnrpc.NewAddressRequest{ - Type: req.Type, - }, - ) - - addr, err := btcutil.DecodeAddress( - addrResp.Address, t.lndHarness.Miner.ActiveNet, - ) - require.NoError(t.t, err) - - addrScript, err := txscript.PayToAddrScript(addr) - require.NoError(t.t, err) - - return &wire.TxOut{ - PkScript: addrScript, - Value: req.Amount, - } - } - - aliceOutputs := fn.Map(reqs, makeOutputs) + aliceOutputs := fn.Map(reqs, func(r *UTXORequest) *wire.TxOut { + return MakeOutput(t, wallet, r.Type, r.Amount) + }) _ = t.lndHarness.Miner.SendOutputsWithoutChange(aliceOutputs, feeRate) t.lndHarness.MineBlocksAndAssertNumTxes(1, 1) @@ -195,7 +192,9 @@ func ResetNodeWallet(t *harnessTest, wallet *node.HarnessNode) { type MintOption func(*MintOptions) type MintOptions struct { - mintingTimeout time.Duration + mintingTimeout time.Duration + siblingBranch *mintrpc.FinalizeBatchRequest_Branch + siblingFullTree *mintrpc.FinalizeBatchRequest_FullTree } func DefaultMintOptions() *MintOptions { @@ -210,6 +209,18 @@ func WithMintingTimeout(timeout time.Duration) MintOption { } } +func WithSiblingBranch(branch mintrpc.FinalizeBatchRequest_Branch) MintOption { + return func(options *MintOptions) { + options.siblingBranch = &branch + } +} + +func WithSiblingTree(tree mintrpc.FinalizeBatchRequest_FullTree) MintOption { + return func(options *MintOptions) { + options.siblingFullTree = &tree + } +} + // MintAssetUnconfirmed is a helper function that mints a batch of assets and // waits until the minting transaction is in the mempool but does not mine a // block. @@ -234,10 +245,17 @@ func MintAssetUnconfirmed(t *testing.T, minerClient *rpcclient.Client, require.Len(t, assetResp.PendingBatch.Assets, idx+1) } + finalizeReq := &mintrpc.FinalizeBatchRequest{} + + if options.siblingBranch != nil { + finalizeReq.BatchSibling = options.siblingBranch + } + if options.siblingFullTree != nil { + finalizeReq.BatchSibling = options.siblingFullTree + } + // Instruct the daemon to finalize the batch. - batchResp, err := tapClient.FinalizeBatch( - ctxt, &mintrpc.FinalizeBatchRequest{}, - ) + batchResp, err := tapClient.FinalizeBatch(ctxt, finalizeReq) require.NoError(t, err) require.NotEmpty(t, batchResp.Batch) require.Len(t, batchResp.Batch.Assets, len(assetRequests)) diff --git a/tapcfg/server.go b/tapcfg/server.go index 13117bb3b..25d1acc53 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -332,6 +332,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, Wallet: walletAnchor, ChainBridge: chainBridge, Log: assetMintingStore, + TreeStore: assetMintingStore, KeyRing: keyRing, GenSigner: virtualTxSigner, GenTxBuilder: &tapscript.GroupTxBuilder{},