Skip to content

Commit 96c5357

Browse files
committed
tapgarden: test minting with batch tapscript tree
1 parent f5fef0f commit 96c5357

File tree

1 file changed

+183
-11
lines changed

1 file changed

+183
-11
lines changed

tapgarden/planter_test.go

Lines changed: 183 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"database/sql"
7+
"encoding/binary"
78
"encoding/hex"
89
"fmt"
910
"math/rand"
@@ -21,6 +22,7 @@ import (
2122
"github.com/davecgh/go-spew/spew"
2223
tap "github.com/lightninglabs/taproot-assets"
2324
"github.com/lightninglabs/taproot-assets/asset"
25+
"github.com/lightninglabs/taproot-assets/commitment"
2426
"github.com/lightninglabs/taproot-assets/fn"
2527
"github.com/lightninglabs/taproot-assets/internal/test"
2628
"github.com/lightninglabs/taproot-assets/proof"
@@ -32,6 +34,7 @@ import (
3234
"github.com/lightningnetwork/lnd/build"
3335
"github.com/lightningnetwork/lnd/keychain"
3436
"github.com/lightningnetwork/lnd/lntest/wait"
37+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
3538
"github.com/lightningnetwork/lnd/ticker"
3639
"github.com/stretchr/testify/require"
3740
)
@@ -244,15 +247,26 @@ type FinalizeBatchResp struct {
244247
// finalizeBatch uses the public FinalizeBatch planter call to start a caretaker
245248
// for an existing batch. The caller must wait for the planter call to complete.
246249
func (t *mintingTestHarness) finalizeBatch(wg *sync.WaitGroup,
247-
respChan chan *FinalizeBatchResp) {
250+
respChan chan *FinalizeBatchResp, params *tapgarden.FinalizeParams) {
248251

249252
t.Helper()
250253

251254
wg.Add(1)
252255
go func() {
253256
defer wg.Done()
254257

255-
frozenBatch, finalizeErr := t.planter.FinalizeBatch(nil)
258+
finalizeParams := tapgarden.FinalizeParams{
259+
FeeRate: fn.None[chainfee.SatPerKWeight](),
260+
SiblingTapTree: fn.None[asset.TapscriptTreeNodes](),
261+
}
262+
263+
if params != nil {
264+
finalizeParams = *params
265+
}
266+
267+
frozenBatch, finalizeErr := t.planter.FinalizeBatch(
268+
finalizeParams,
269+
)
256270
resp := &FinalizeBatchResp{
257271
Batch: frozenBatch,
258272
Err: finalizeErr,
@@ -263,7 +277,8 @@ func (t *mintingTestHarness) finalizeBatch(wg *sync.WaitGroup,
263277
}
264278

265279
func (t *mintingTestHarness) assertFinalizeBatch(wg *sync.WaitGroup,
266-
respChan chan *FinalizeBatchResp, errString string) {
280+
respChan chan *FinalizeBatchResp,
281+
errString string) *tapgarden.MintingBatch {
267282

268283
t.Helper()
269284

@@ -273,16 +288,18 @@ func (t *mintingTestHarness) assertFinalizeBatch(wg *sync.WaitGroup,
273288
switch {
274289
case errString == "":
275290
require.NoError(t, finalizeResp.Err)
291+
return finalizeResp.Batch
276292

277293
default:
278294
require.ErrorContains(t, finalizeResp.Err, errString)
295+
return nil
279296
}
280297
}
281298

282299
// progressCaretaker uses the mock interfaces to progress a caretaker from start
283300
// to TX confirmation.
284-
func (t *mintingTestHarness) progressCaretaker(
285-
seedlings []*tapgarden.Seedling, batchSibling *chainhash.Hash) func() {
301+
func (t *mintingTestHarness) progressCaretaker(seedlings []*tapgarden.Seedling,
302+
batchSibling *commitment.TapscriptPreimage) func() {
286303

287304
// Assert that the caretaker has requested a genesis TX to be funded.
288305
_ = t.assertGenesisTxFunded()
@@ -632,7 +649,7 @@ func (t *mintingTestHarness) assertSeedlingsMatchSprouts(
632649
// assertGenesisPsbtFinalized asserts that a request to finalize the genesis
633650
// transaction has been requested by a caretaker.
634651
func (t *mintingTestHarness) assertGenesisPsbtFinalized(
635-
sibling *chainhash.Hash) {
652+
sibling *commitment.TapscriptPreimage) {
636653

637654
t.Helper()
638655

@@ -1115,7 +1132,7 @@ func testFinalizeBatch(t *mintingTestHarness) {
11151132
)
11161133

11171134
// Finalize the pending batch to start a caretaker.
1118-
t.finalizeBatch(&wg, respChan)
1135+
t.finalizeBatch(&wg, respChan, nil)
11191136
batchCount++
11201137

11211138
_, err := fn.RecvOrTimeout(
@@ -1141,7 +1158,7 @@ func testFinalizeBatch(t *mintingTestHarness) {
11411158
// caretaker to TX confirmation. The finalize call should report no
11421159
// error, but the caretaker should propagate the confirmation error to
11431160
// the shared error channel.
1144-
t.finalizeBatch(&wg, respChan)
1161+
t.finalizeBatch(&wg, respChan, nil)
11451162
batchCount++
11461163

11471164
_ = t.progressCaretaker(seedlings, nil)
@@ -1165,7 +1182,7 @@ func testFinalizeBatch(t *mintingTestHarness) {
11651182
t.chain.EmptyConf(true)
11661183

11671184
// Start a new caretaker that should reach TX broadcast.
1168-
t.finalizeBatch(&wg, respChan)
1185+
t.finalizeBatch(&wg, respChan, nil)
11691186
batchCount++
11701187

11711188
sendConfNtfn := t.progressCaretaker(seedlings, nil)
@@ -1187,15 +1204,15 @@ func testFinalizeBatch(t *mintingTestHarness) {
11871204

11881205
// If we try to finalize without a pending batch, the finalize call
11891206
// should return an error.
1190-
t.finalizeBatch(&wg, respChan)
1207+
t.finalizeBatch(&wg, respChan, nil)
11911208
t.assertFinalizeBatch(&wg, respChan, "no pending batch")
11921209
t.assertNumCaretakersActive(caretakerCount)
11931210

11941211
// Queue another batch and drive the caretaker to a successful minting.
11951212
seedlings = t.queueInitialBatch(numSeedlings)
11961213
t.chain.EmptyConf(false)
11971214

1198-
t.finalizeBatch(&wg, respChan)
1215+
t.finalizeBatch(&wg, respChan, nil)
11991216
batchCount++
12001217

12011218
sendConfNtfn = t.progressCaretaker(seedlings, nil)
@@ -1208,6 +1225,156 @@ func testFinalizeBatch(t *mintingTestHarness) {
12081225
t.assertLastBatchState(batchCount, tapgarden.BatchStateFinalized)
12091226
}
12101227

1228+
func testFinalizeWithTapscriptTree(t *mintingTestHarness) {
1229+
// First, create a new chain planter instance using the supplied test
1230+
// harness.
1231+
t.refreshChainPlanter()
1232+
1233+
// Create an initial batch of 5 seedlings.
1234+
const numSeedlings = 5
1235+
seedlings := t.queueInitialBatch(numSeedlings)
1236+
1237+
var (
1238+
wg sync.WaitGroup
1239+
respChan = make(chan *FinalizeBatchResp, 1)
1240+
finalizeReq tapgarden.FinalizeParams
1241+
batchCount = 0
1242+
)
1243+
1244+
// Build a standalone tapscript tree object, that matches the tree
1245+
// created by other test helpers.
1246+
sigLockKey := test.RandPubKey(t)
1247+
hashLockWitness := []byte("foobar")
1248+
hashLockLeaf := test.ScriptHashLock(t.T, hashLockWitness)
1249+
sigLeaf := test.ScriptSchnorrSig(t.T, sigLockKey)
1250+
tapTreePreimage, err := asset.TapTreeNodesFromLeaves(
1251+
[]txscript.TapLeaf{hashLockLeaf, sigLeaf},
1252+
)
1253+
require.NoError(t, err)
1254+
1255+
finalizeReq = tapgarden.FinalizeParams{
1256+
SiblingTapTree: fn.Some(*tapTreePreimage),
1257+
}
1258+
1259+
// Force tapscript tree storage to fail, which should cause batch
1260+
// finalization to fail.
1261+
t.treeStore.FailStore = true
1262+
t.finalizeBatch(&wg, respChan, &finalizeReq)
1263+
finalizeErr := <-respChan
1264+
require.ErrorContains(t, finalizeErr.Err, "unable to store")
1265+
1266+
// Empty the main error channel before reattempting a mint.
1267+
select {
1268+
case <-t.errChan:
1269+
default:
1270+
}
1271+
1272+
// Allow tapscript tree storage to succeed, but force tapscript tree
1273+
// loading to fail.
1274+
t.treeStore.FailStore = false
1275+
t.treeStore.FailLoad = true
1276+
1277+
// Receive all the signals needed to progress the caretaker through
1278+
// the batch sprouting, which is when the sibling tapscript tree is
1279+
// used.
1280+
progressCaretakerToTxSigning := func(
1281+
currentSeedlings []*tapgarden.Seedling) {
1282+
1283+
_ = t.assertGenesisTxFunded()
1284+
1285+
for i := 0; i < len(currentSeedlings); i++ {
1286+
t.assertKeyDerived()
1287+
1288+
if currentSeedlings[i].EnableEmission {
1289+
t.assertKeyDerived()
1290+
}
1291+
}
1292+
}
1293+
1294+
// Finalize the batch with a tapscript tree sibling.
1295+
t.finalizeBatch(&wg, respChan, &finalizeReq)
1296+
batchCount++
1297+
1298+
// The caretaker should fail when computing the Taproot output key.
1299+
progressCaretakerToTxSigning(seedlings)
1300+
t.assertFinalizeBatch(&wg, respChan, "failed to load tapscript tree")
1301+
t.assertLastBatchState(batchCount, tapgarden.BatchStateFrozen)
1302+
t.assertNoPendingBatch()
1303+
1304+
// Reset the tapscript tree store to not force load or store failures.
1305+
t.treeStore.FailStore = false
1306+
t.treeStore.FailLoad = false
1307+
1308+
// Construct a tapscript tree with a single leaf that has the structure
1309+
// of a TapLeaf computed from a TapCommitment. This should be rejected
1310+
// by the caretaker, as the genesis TX for the batch should only commit
1311+
// to one TapCommitment.
1312+
var dummyRootSum [8]byte
1313+
binary.BigEndian.PutUint64(dummyRootSum[:], test.RandInt[uint64]())
1314+
dummyRootHashParts := [][]byte{
1315+
{byte(asset.V0)}, commitment.TaprootAssetsMarker[:],
1316+
fn.ByteSlice(test.RandHash()), dummyRootSum[:],
1317+
}
1318+
dummyTapCommitmentRootHash := bytes.Join(dummyRootHashParts, nil)
1319+
dummyTapLeaf := txscript.NewBaseTapLeaf(dummyTapCommitmentRootHash)
1320+
dummyTapCommitmentPreimage, err := asset.TapTreeNodesFromLeaves(
1321+
[]txscript.TapLeaf{dummyTapLeaf},
1322+
)
1323+
require.NoError(t, err)
1324+
1325+
finalizeReq.SiblingTapTree = fn.Some(*dummyTapCommitmentPreimage)
1326+
1327+
// Queue another batch, and try to finalize with a sibling that is also
1328+
// a Taproot asset commitment.
1329+
seedlings = t.queueInitialBatch(numSeedlings)
1330+
t.finalizeBatch(&wg, respChan, &finalizeReq)
1331+
batchCount++
1332+
1333+
progressCaretakerToTxSigning(seedlings)
1334+
t.assertFinalizeBatch(
1335+
&wg, respChan, "preimage is a Taproot Asset commitment",
1336+
)
1337+
t.assertNoPendingBatch()
1338+
1339+
// Queue another batch, and provide a valid sibling tapscript tree.
1340+
seedlings = t.queueInitialBatch(numSeedlings)
1341+
finalizeReq.SiblingTapTree = fn.Some(*tapTreePreimage)
1342+
t.finalizeBatch(&wg, respChan, &finalizeReq)
1343+
batchCount++
1344+
1345+
// Verify that the final genesis TX uses the correct Taproot output key.
1346+
treeRootChildren := test.BuildTapscriptTreeNoReveal(t.T, sigLockKey)
1347+
siblingPreimage := commitment.NewPreimageFromBranch(treeRootChildren)
1348+
sendConfNtfn := t.progressCaretaker(seedlings, &siblingPreimage)
1349+
sendConfNtfn()
1350+
1351+
// Once the TX is broadcast, the caretaker should run to completion,
1352+
// storing issuance proofs and updating the batch state to finalized.
1353+
batchWithSibling := t.assertFinalizeBatch(&wg, respChan, "")
1354+
require.NotNil(t, batchWithSibling)
1355+
t.assertNoError()
1356+
t.assertNoPendingBatch()
1357+
t.assertNumCaretakersActive(0)
1358+
t.assertLastBatchState(batchCount, tapgarden.BatchStateFinalized)
1359+
1360+
// Verify that the final minting output key matches what we would derive
1361+
// manually.
1362+
batchRootCommitment := batchWithSibling.RootAssetCommitment
1363+
require.NotNil(t, batchRootCommitment)
1364+
siblingHash, err := siblingPreimage.TapHash()
1365+
require.NoError(t, err)
1366+
batchScriptRoot := batchRootCommitment.TapscriptRoot(siblingHash)
1367+
batchOutputKeyExpected := txscript.ComputeTaprootOutputKey(
1368+
batchWithSibling.BatchKey.PubKey, batchScriptRoot[:],
1369+
)
1370+
batchOutputKey, _, err := batchWithSibling.MintingOutputKey(nil)
1371+
require.NoError(t, err)
1372+
require.Equal(
1373+
t, batchOutputKeyExpected.SerializeCompressed(),
1374+
batchOutputKey.SerializeCompressed(),
1375+
)
1376+
}
1377+
12111378
// mintingStoreTestCase is used to programmatically run a series of test cases
12121379
// that are parametrized based on a fresh minting store.
12131380
type mintingStoreTestCase struct {
@@ -1238,6 +1405,11 @@ var testCases = []mintingStoreTestCase{
12381405
interval: minterInterval,
12391406
testFunc: testFinalizeBatch,
12401407
},
1408+
{
1409+
name: "finalize_with_tapscript_tree",
1410+
interval: minterInterval,
1411+
testFunc: testFinalizeWithTapscriptTree,
1412+
},
12411413
}
12421414

12431415
// TestBatchedAssetIssuance runs a test of tests to ensure that the set of

0 commit comments

Comments
 (0)