Skip to content

Commit 35c8156

Browse files
committed
itest: add script path spend of minting output
1 parent 96c5357 commit 35c8156

File tree

5 files changed

+211
-27
lines changed

5 files changed

+211
-27
lines changed

itest/assertions.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/btcsuite/btcd/rpcclient"
1818
"github.com/btcsuite/btcd/wire"
1919
"github.com/lightninglabs/taproot-assets/asset"
20+
"github.com/lightninglabs/taproot-assets/commitment"
2021
"github.com/lightninglabs/taproot-assets/fn"
2122
"github.com/lightninglabs/taproot-assets/proof"
2223
"github.com/lightninglabs/taproot-assets/taprpc"
@@ -1562,6 +1563,30 @@ func AssertAssetsMinted(t *testing.T,
15621563
return assetList
15631564
}
15641565

1566+
func AssertGenesisOutput(t *testing.T, output *taprpc.ManagedUtxo,
1567+
sibling commitment.TapscriptPreimage) {
1568+
1569+
// Fetch the encoded tapscript sibling from an anchored asset, and check
1570+
// it against the expected sibling.
1571+
require.True(t, len(output.Assets) > 1)
1572+
rpcSibling := output.Assets[0].ChainAnchor.TapscriptSibling
1573+
require.True(t, fn.All(output.Assets, func(a *taprpc.Asset) bool {
1574+
return bytes.Equal(a.ChainAnchor.TapscriptSibling, rpcSibling)
1575+
}))
1576+
encodedSibling, siblingHash, err := commitment.
1577+
MaybeEncodeTapscriptPreimage(&sibling)
1578+
require.NoError(t, err)
1579+
require.Equal(t, encodedSibling, rpcSibling)
1580+
1581+
// We should be able to recompute a merkle root from the tapscript
1582+
// sibling hash and the Taproot Asset Commitment root that matches what
1583+
// is stored in the managed output.
1584+
expectedMerkleRoot := asset.NewTapBranchHash(
1585+
(chainhash.Hash)(output.TaprootAssetRoot), *siblingHash,
1586+
)
1587+
require.Equal(t, expectedMerkleRoot[:], output.MerkleRoot)
1588+
}
1589+
15651590
func AssertAssetBalances(t *testing.T, client taprpc.TaprootAssetsClient,
15661591
simpleAssets, issuableAssets []*taprpc.Asset) {
15671592

itest/assets_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
package itest
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/tls"
67
"net/http"
78
"time"
89

10+
"github.com/btcsuite/btcd/btcec/v2"
911
"github.com/btcsuite/btcd/chaincfg/chainhash"
12+
"github.com/btcsuite/btcd/txscript"
1013
"github.com/btcsuite/btcd/wire"
14+
"github.com/lightninglabs/taproot-assets/asset"
15+
"github.com/lightninglabs/taproot-assets/commitment"
1116
"github.com/lightninglabs/taproot-assets/fn"
17+
"github.com/lightninglabs/taproot-assets/internal/test"
1218
"github.com/lightninglabs/taproot-assets/proof"
1319
"github.com/lightninglabs/taproot-assets/taprpc"
1420
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
1521
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
22+
"github.com/lightningnetwork/lnd/lnrpc"
23+
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
1624
"github.com/stretchr/testify/require"
25+
"golang.org/x/exp/maps"
1726
"golang.org/x/net/http2"
1827
)
1928

@@ -334,3 +343,130 @@ func testMintAssetNameCollisionError(t *harnessTest) {
334343
collideAssetName := rpcCollideAsset[0].AssetGenesis.Name
335344
require.Equal(t.t, commonAssetName, collideAssetName)
336345
}
346+
347+
// testMintAssetsWithTapscriptSibling tests that a batch of assets can be minted
348+
// with a tapscript sibling, and that the genesis output from that mint can be
349+
// spend via the script path.
350+
func testMintAssetsWithTapscriptSibling(t *harnessTest) {
351+
ctxb := context.Background()
352+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
353+
defer cancel()
354+
355+
// Build the tapscript tree.
356+
sigLockPrivKey := test.RandPrivKey(t.t)
357+
hashLockPreimage := []byte("foobar")
358+
hashLockLeaf := test.ScriptHashLock(t.t, hashLockPreimage)
359+
sigLeaf := test.ScriptSchnorrSig(t.t, sigLockPrivKey.PubKey())
360+
siblingTree := txscript.AssembleTaprootScriptTree(hashLockLeaf, sigLeaf)
361+
362+
siblingBranch := txscript.NewTapBranch(
363+
siblingTree.RootNode.Left(), siblingTree.RootNode.Right(),
364+
)
365+
siblingPreimage := commitment.NewPreimageFromBranch(siblingBranch)
366+
typedBranch := asset.TapTreeNodesFromBranch(siblingBranch)
367+
rawBranch := fn.MapOptionZ(asset.GetBranch(typedBranch), asset.ToBranch)
368+
require.Len(t.t, rawBranch, 2)
369+
siblingReq := mintrpc.FinalizeBatchRequest_Branch{
370+
Branch: &taprpc.TapBranch{
371+
LeftTaphash: rawBranch[0],
372+
RightTaphash: rawBranch[1],
373+
},
374+
}
375+
376+
rpcSimpleAssets := MintAssetsConfirmBatch(
377+
t.t, t.lndHarness.Miner.Client, t.tapd, simpleAssets,
378+
WithSiblingBranch(siblingReq),
379+
)
380+
rpcIssuableAssets := MintAssetsConfirmBatch(
381+
t.t, t.lndHarness.Miner.Client, t.tapd, issuableAssets,
382+
)
383+
384+
AssertAssetBalances(t.t, t.tapd, rpcSimpleAssets, rpcIssuableAssets)
385+
386+
// Filter the managed UTXOs to select the genesis UTXO with the
387+
// tapscript sibling.
388+
utxos, err := t.tapd.ListUtxos(ctxt, &taprpc.ListUtxosRequest{})
389+
require.NoError(t.t, err)
390+
391+
utxoWithTapSibling := func(utxo *taprpc.ManagedUtxo) bool {
392+
return !bytes.Equal(utxo.TaprootAssetRoot, utxo.MerkleRoot)
393+
}
394+
mintingOutputWithSibling := fn.Filter(
395+
maps.Values(utxos.ManagedUtxos), utxoWithTapSibling,
396+
)
397+
require.Len(t.t, mintingOutputWithSibling, 1)
398+
genesisWithSibling := mintingOutputWithSibling[0]
399+
400+
// Verify that all assets anchored in the output with the tapscript
401+
// sibling have the correct sibling preimage. Also verify that the final
402+
// tweak used for the genesis output is derived from the tapscript
403+
// sibling created above and the batch Taproot Asset commitment.
404+
AssertGenesisOutput(t.t, genesisWithSibling, siblingPreimage)
405+
406+
// Extract the fields needed to construct a script path spend, which
407+
// includes the Taproot Asset commitment root, the final tap tweak, and
408+
// the internal key.
409+
mintTapTweak := genesisWithSibling.MerkleRoot
410+
mintTapTreeRoot := genesisWithSibling.TaprootAssetRoot
411+
mintInternalKey, err := btcec.ParsePubKey(
412+
genesisWithSibling.InternalKey,
413+
)
414+
require.NoError(t.t, err)
415+
416+
mintOutputKey := txscript.ComputeTaprootOutputKey(
417+
mintInternalKey, mintTapTweak,
418+
)
419+
mintOutputKeyIsOdd := mintOutputKey.SerializeCompressed()[0] == 0x03
420+
siblingScriptHash := sigLeaf.TapHash()
421+
422+
// Build the control block and witness.
423+
inclusionProof := bytes.Join(
424+
[][]byte{siblingScriptHash[:], mintTapTreeRoot}, nil,
425+
)
426+
hashLockControlBlock := txscript.ControlBlock{
427+
InternalKey: mintInternalKey,
428+
OutputKeyYIsOdd: mintOutputKeyIsOdd,
429+
LeafVersion: txscript.BaseLeafVersion,
430+
InclusionProof: inclusionProof,
431+
}
432+
hashLockControlBlockBytes, err := hashLockControlBlock.ToBytes()
433+
require.NoError(t.t, err)
434+
435+
hashLockWitness := wire.TxWitness{
436+
hashLockPreimage, hashLockLeaf.Script, hashLockControlBlockBytes,
437+
}
438+
439+
// Make a non-tap output from Bob to use in a TX spending Alice's
440+
// genesis UTXO.
441+
burnOutput := MakeOutput(
442+
t, t.lndHarness.Bob, lnrpc.AddressType_TAPROOT_PUBKEY, 500,
443+
)
444+
445+
// Construct and publish the TX.
446+
genesisOutpoint, err := wire.NewOutPointFromString(
447+
genesisWithSibling.OutPoint,
448+
)
449+
require.NoError(t.t, err)
450+
451+
burnTx := wire.MsgTx{
452+
Version: 2,
453+
TxIn: []*wire.TxIn{{
454+
PreviousOutPoint: *genesisOutpoint,
455+
Witness: hashLockWitness,
456+
}},
457+
TxOut: []*wire.TxOut{burnOutput},
458+
}
459+
460+
var burnTxBuf bytes.Buffer
461+
require.NoError(t.t, burnTx.Serialize(&burnTxBuf))
462+
t.lndHarness.Bob.RPC.PublishTransaction(&walletrpc.Transaction{
463+
TxHex: burnTxBuf.Bytes(),
464+
})
465+
466+
// Bob should detect the TX, and the resulting confirmed UTXO once
467+
// a new block is mined.
468+
t.lndHarness.Miner.AssertNumTxsInMempool(1)
469+
t.lndHarness.AssertNumUTXOsUnconfirmed(t.lndHarness.Bob, 1)
470+
t.lndHarness.MineBlocksAndAssertNumTxes(1, 1)
471+
t.lndHarness.AssertNumUTXOsWithConf(t.lndHarness.Bob, 1, 1, 1)
472+
}

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ var testCases = []*testCase{
1717
name: "asset name collision raises mint error",
1818
test: testMintAssetNameCollisionError,
1919
},
20+
{
21+
name: "mint assets with tap sibling",
22+
test: testMintAssetsWithTapscriptSibling,
23+
},
2024
{
2125
name: "addresses",
2226
test: testAddresses,

itest/utils.go

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/btcsuite/btcd/chaincfg"
1010
"github.com/btcsuite/btcd/chaincfg/chainhash"
1111
"github.com/btcsuite/btcd/rpcclient"
12-
"github.com/btcsuite/btcd/txscript"
1312
"github.com/btcsuite/btcd/wire"
1413
"github.com/lightninglabs/taproot-assets/asset"
1514
"github.com/lightninglabs/taproot-assets/fn"
@@ -130,6 +129,23 @@ type UTXORequest struct {
130129
Amount int64
131130
}
132131

132+
// MakeOutput creates a new TXO from a given output type and amount.
133+
func MakeOutput(t *harnessTest, wallet *node.HarnessNode,
134+
addrType lnrpc.AddressType, amount int64) *wire.TxOut {
135+
136+
addrResp := wallet.RPC.NewAddress(&lnrpc.NewAddressRequest{
137+
Type: addrType,
138+
})
139+
addr, err := btcutil.DecodeAddress(
140+
addrResp.Address, harnessNetParams,
141+
)
142+
require.NoError(t.t, err)
143+
144+
addrScript := t.lndHarness.PayToAddrScript(addr)
145+
146+
return wire.NewTxOut(amount, addrScript)
147+
}
148+
133149
// SetNodeUTXOs sets the wallet state for the given node wallet to a set of
134150
// UTXOs of a specific type and value.
135151
func SetNodeUTXOs(t *harnessTest, wallet *node.HarnessNode,
@@ -146,28 +162,9 @@ func SetNodeUTXOs(t *harnessTest, wallet *node.HarnessNode,
146162

147163
// Build TXOs from the UTXO requests, which will be used by the miner
148164
// to build a TX.
149-
makeOutputs := func(req *UTXORequest) *wire.TxOut {
150-
addrResp := wallet.RPC.NewAddress(
151-
&lnrpc.NewAddressRequest{
152-
Type: req.Type,
153-
},
154-
)
155-
156-
addr, err := btcutil.DecodeAddress(
157-
addrResp.Address, t.lndHarness.Miner.ActiveNet,
158-
)
159-
require.NoError(t.t, err)
160-
161-
addrScript, err := txscript.PayToAddrScript(addr)
162-
require.NoError(t.t, err)
163-
164-
return &wire.TxOut{
165-
PkScript: addrScript,
166-
Value: req.Amount,
167-
}
168-
}
169-
170-
aliceOutputs := fn.Map(reqs, makeOutputs)
165+
aliceOutputs := fn.Map(reqs, func(r *UTXORequest) *wire.TxOut {
166+
return MakeOutput(t, wallet, r.Type, r.Amount)
167+
})
171168

172169
_ = t.lndHarness.Miner.SendOutputsWithoutChange(aliceOutputs, feeRate)
173170
t.lndHarness.MineBlocksAndAssertNumTxes(1, 1)
@@ -195,7 +192,9 @@ func ResetNodeWallet(t *harnessTest, wallet *node.HarnessNode) {
195192
type MintOption func(*MintOptions)
196193

197194
type MintOptions struct {
198-
mintingTimeout time.Duration
195+
mintingTimeout time.Duration
196+
siblingBranch *mintrpc.FinalizeBatchRequest_Branch
197+
siblingFullTree *mintrpc.FinalizeBatchRequest_FullTree
199198
}
200199

201200
func DefaultMintOptions() *MintOptions {
@@ -210,6 +209,18 @@ func WithMintingTimeout(timeout time.Duration) MintOption {
210209
}
211210
}
212211

212+
func WithSiblingBranch(branch mintrpc.FinalizeBatchRequest_Branch) MintOption {
213+
return func(options *MintOptions) {
214+
options.siblingBranch = &branch
215+
}
216+
}
217+
218+
func WithSiblingTree(tree mintrpc.FinalizeBatchRequest_FullTree) MintOption {
219+
return func(options *MintOptions) {
220+
options.siblingFullTree = &tree
221+
}
222+
}
223+
213224
// MintAssetUnconfirmed is a helper function that mints a batch of assets and
214225
// waits until the minting transaction is in the mempool but does not mine a
215226
// block.
@@ -234,10 +245,17 @@ func MintAssetUnconfirmed(t *testing.T, minerClient *rpcclient.Client,
234245
require.Len(t, assetResp.PendingBatch.Assets, idx+1)
235246
}
236247

248+
finalizeReq := &mintrpc.FinalizeBatchRequest{}
249+
250+
if options.siblingBranch != nil {
251+
finalizeReq.BatchSibling = options.siblingBranch
252+
}
253+
if options.siblingFullTree != nil {
254+
finalizeReq.BatchSibling = options.siblingFullTree
255+
}
256+
237257
// Instruct the daemon to finalize the batch.
238-
batchResp, err := tapClient.FinalizeBatch(
239-
ctxt, &mintrpc.FinalizeBatchRequest{},
240-
)
258+
batchResp, err := tapClient.FinalizeBatch(ctxt, finalizeReq)
241259
require.NoError(t, err)
242260
require.NotEmpty(t, batchResp.Batch)
243261
require.Len(t, batchResp.Batch.Assets, len(assetRequests))

tapcfg/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
332332
Wallet: walletAnchor,
333333
ChainBridge: chainBridge,
334334
Log: assetMintingStore,
335+
TreeStore: assetMintingStore,
335336
KeyRing: keyRing,
336337
GenSigner: virtualTxSigner,
337338
GenTxBuilder: &tapscript.GroupTxBuilder{},

0 commit comments

Comments
 (0)