Skip to content

Commit

Permalink
itest: add script path spend of minting output
Browse files Browse the repository at this point in the history
  • Loading branch information
jharveyb committed Feb 26, 2024
1 parent 96c5357 commit 35c8156
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 27 deletions.
25 changes: 25 additions & 0 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {

Expand Down
136 changes: 136 additions & 0 deletions itest/assets_test.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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)
}
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
72 changes: 45 additions & 27 deletions itest/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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))
Expand Down
1 change: 1 addition & 0 deletions tapcfg/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
Expand Down

0 comments on commit 35c8156

Please sign in to comment.