Skip to content

Commit

Permalink
tapgarden: test minting with batch tapscript tree
Browse files Browse the repository at this point in the history
  • Loading branch information
jharveyb committed Feb 22, 2024
1 parent 1c8dda0 commit 36c09b1
Showing 1 changed file with 184 additions and 12 deletions.
196 changes: 184 additions & 12 deletions tapgarden/planter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"bytes"
"context"
"database/sql"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/lightninglabs/taproot-assets/tapsend"
"math/rand"
"sync"
"testing"
Expand All @@ -22,16 +22,19 @@ import (
"github.com/davecgh/go-spew/spew"
tap "github.com/lightninglabs/taproot-assets"
"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/tapdb"
_ "github.com/lightninglabs/taproot-assets/tapdb" // Register relevant drivers.
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tapscript"
"github.com/lightninglabs/taproot-assets/tapsend"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/ticker"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -244,15 +247,26 @@ type FinalizeBatchResp struct {
// finalizeBatch uses the public FinalizeBatch planter call to start a caretaker
// for an existing batch. The caller must wait for the planter call to complete.
func (t *mintingTestHarness) finalizeBatch(wg *sync.WaitGroup,
respChan chan *FinalizeBatchResp) {
respChan chan *FinalizeBatchResp, params *tapgarden.FinalizeParams) {

t.Helper()

wg.Add(1)
go func() {
defer wg.Done()

frozenBatch, finalizeErr := t.planter.FinalizeBatch(nil)
finalizeParams := tapgarden.FinalizeParams{
FeeRate: fn.None[chainfee.SatPerKWeight](),
TapTree: fn.None[asset.TapscriptTreeNodes](),
}

if params != nil {
finalizeParams = *params
}

frozenBatch, finalizeErr := t.planter.FinalizeBatch(
finalizeParams,
)
resp := &FinalizeBatchResp{
Batch: frozenBatch,
Err: finalizeErr,
Expand All @@ -263,7 +277,8 @@ func (t *mintingTestHarness) finalizeBatch(wg *sync.WaitGroup,
}

func (t *mintingTestHarness) assertFinalizeBatch(wg *sync.WaitGroup,
respChan chan *FinalizeBatchResp, errString string) {
respChan chan *FinalizeBatchResp,
errString string) *tapgarden.MintingBatch {

t.Helper()

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

default:
require.ErrorContains(t, finalizeResp.Err, errString)
return nil
}
}

// progressCaretaker uses the mock interfaces to progress a caretaker from start
// to TX confirmation.
func (t *mintingTestHarness) progressCaretaker(
seedlings []*tapgarden.Seedling, batchSibling *chainhash.Hash) func() {
func (t *mintingTestHarness) progressCaretaker(seedlings []*tapgarden.Seedling,
batchSibling *commitment.TapscriptPreimage) func() {

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

t.Helper()

Expand Down Expand Up @@ -1115,7 +1132,7 @@ func testFinalizeBatch(t *mintingTestHarness) {
)

// Finalize the pending batch to start a caretaker.
t.finalizeBatch(&wg, respChan)
t.finalizeBatch(&wg, respChan, nil)
batchCount++

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

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

// Start a new caretaker that should reach TX broadcast.
t.finalizeBatch(&wg, respChan)
t.finalizeBatch(&wg, respChan, nil)
batchCount++

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

// If we try to finalize without a pending batch, the finalize call
// should return an error.
t.finalizeBatch(&wg, respChan)
t.finalizeBatch(&wg, respChan, nil)
t.assertFinalizeBatch(&wg, respChan, "no pending batch")
t.assertNumCaretakersActive(caretakerCount)

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

t.finalizeBatch(&wg, respChan)
t.finalizeBatch(&wg, respChan, nil)
batchCount++

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

func testFinalizeWithTapscriptTree(t *mintingTestHarness) {
// First, create a new chain planter instance using the supplied test
// harness.
t.refreshChainPlanter()

// Create an initial batch of 5 seedlings.
const numSeedlings = 5
seedlings := t.queueInitialBatch(numSeedlings)

var (
wg sync.WaitGroup
respChan = make(chan *FinalizeBatchResp, 1)
finalizeReq tapgarden.FinalizeParams
batchCount = 0
)

// Build a standalone tapscript tree object, that matches the tree
// created by other test helpers.
sigLockKey := test.RandPubKey(t)
hashLockWitness := []byte("foobar")
hashLockLeaf := test.ScriptHashLock(t.T, hashLockWitness)
sigLeaf := test.ScriptSchnorrSig(t.T, sigLockKey)
tapTreePreimage, err := asset.TapTreeNodesFromLeaves(
[]txscript.TapLeaf{hashLockLeaf, sigLeaf},
)
require.NoError(t, err)

finalizeReq = tapgarden.FinalizeParams{
TapTree: fn.Some(*tapTreePreimage),
}

// Force tapscript tree storage to fail, which should cause batch
// finalization to fail.
t.treeStore.FailStore = true
t.finalizeBatch(&wg, respChan, &finalizeReq)
finalizeErr := <-respChan
require.ErrorContains(t, finalizeErr.Err, "unable to store")

// Empty the main error channel before reattempting a mint.
select {
case <-t.errChan:
default:
}

// Allow tapscript tree storage to succeed, but force tapscript tree
// loading to fail.
t.treeStore.FailStore = false
t.treeStore.FailLoad = true

// Receive all the signals needed to progress the caretaker through
// the batch sprouting, which is when the sibling tapscript tree is
// used.
progressCaretakerToTxSigning := func(
currentSeedlings []*tapgarden.Seedling) {

_ = t.assertGenesisTxFunded()

for i := 0; i < len(currentSeedlings); i++ {
t.assertKeyDerived()

if currentSeedlings[i].EnableEmission {
t.assertKeyDerived()
}
}
}

// Finalize the batch with a tapscript tree sibling.
t.finalizeBatch(&wg, respChan, &finalizeReq)
batchCount++

// The caretaker should fail when computing the Taproot output key.
progressCaretakerToTxSigning(seedlings)
t.assertFinalizeBatch(&wg, respChan, "failed to load tapscript tree")
t.assertLastBatchState(batchCount, tapgarden.BatchStateFrozen)
t.assertNoPendingBatch()

// Reset the tapscript tree store to not force load or store failures.
t.treeStore.FailStore = false
t.treeStore.FailLoad = false

// Construct a tapscript tree with a single leaf that has the structure
// of a TapLeaf computed from a TapCommitment. This should be rejected
// by the caretaker, as the genesis TX for the batch should only commit
// to one TapCommitment.
var dummyRootSum [8]byte
binary.BigEndian.PutUint64(dummyRootSum[:], test.RandInt[uint64]())
dummyRootHashParts := [][]byte{
{byte(asset.V0)}, commitment.TaprootAssetsMarker[:],
fn.ByteSlice(test.RandHash()), dummyRootSum[:],
}
dummyTapCommitmentRootHash := bytes.Join(dummyRootHashParts, nil)
dummyTapLeaf := txscript.NewBaseTapLeaf(dummyTapCommitmentRootHash)
dummyTapCommitmentPreimage, err := asset.TapTreeNodesFromLeaves(
[]txscript.TapLeaf{dummyTapLeaf},
)
require.NoError(t, err)

finalizeReq.TapTree = fn.Some(*dummyTapCommitmentPreimage)

// Queue another batch, and try to finalize with a sibling that is also
// a Taproot asset commitment.
seedlings = t.queueInitialBatch(numSeedlings)
t.finalizeBatch(&wg, respChan, &finalizeReq)
batchCount++

progressCaretakerToTxSigning(seedlings)
t.assertFinalizeBatch(
&wg, respChan, "preimage is a Taproot Asset commitment",
)
t.assertNoPendingBatch()

// Queue another batch, and provide a valid sibling tapscript tree.
seedlings = t.queueInitialBatch(numSeedlings)
finalizeReq.TapTree = fn.Some(*tapTreePreimage)
t.finalizeBatch(&wg, respChan, &finalizeReq)
batchCount++

// Verify that the final genesis TX uses the correct Taproot output key.
treeRootChildren := test.BuildTapscriptTreeNoReveal(t.T, sigLockKey)
siblingPreimage := commitment.NewPreimageFromBranch(treeRootChildren)
sendConfNtfn := t.progressCaretaker(seedlings, &siblingPreimage)
sendConfNtfn()

// Once the TX is broadcast, the caretaker should run to completion,
// storing issuance proofs and updating the batch state to finalized.
batchWithSibling := t.assertFinalizeBatch(&wg, respChan, "")
require.NotNil(t, batchWithSibling)
t.assertNoError()
t.assertNoPendingBatch()
t.assertNumCaretakersActive(0)
t.assertLastBatchState(batchCount, tapgarden.BatchStateFinalized)

// Verify that the final minting output key matches what we would derive
// manually.
batchRootCommitment := batchWithSibling.RootAssetCommitment
require.NotNil(t, batchRootCommitment)
siblingHash, err := siblingPreimage.TapHash()
require.NoError(t, err)
batchScriptRoot := batchRootCommitment.TapscriptRoot(siblingHash)
batchOutputKeyExpected := txscript.ComputeTaprootOutputKey(
batchWithSibling.BatchKey.PubKey, batchScriptRoot[:],
)
batchOutputKey, _, err := batchWithSibling.MintingOutputKey(nil)
require.NoError(t, err)
require.Equal(
t, batchOutputKeyExpected.SerializeCompressed(),
batchOutputKey.SerializeCompressed(),
)
}

// mintingStoreTestCase is used to programmatically run a series of test cases
// that are parametrized based on a fresh minting store.
type mintingStoreTestCase struct {
Expand Down Expand Up @@ -1238,6 +1405,11 @@ var testCases = []mintingStoreTestCase{
interval: minterInterval,
testFunc: testFinalizeBatch,
},
{
name: "finalize_with_tapscript_tree",
interval: minterInterval,
testFunc: testFinalizeWithTapscriptTree,
},
}

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

0 comments on commit 36c09b1

Please sign in to comment.