Skip to content

Commit 032061e

Browse files
committed
itest: add test for minting with external group key via chantools
Add a new integration test, `testMintExternalGroupKeyChantools`, to validate minting two assets into the same asset group across two different batches. The test ensures asset group signatures are generated using chantools with an externally managed signing key for each batch.
1 parent e0dba5b commit 032061e

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed

itest/mint_fund_seal_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,91 @@ func testMintFundSealAssets(t *harnessTest) {
501501
})
502502
}
503503

504+
// testMintExternalGroupKeyChantools tests that we're able to mint an asset
505+
// using an external asset signing group key derived and managed by chantools.
506+
func testMintExternalGroupKeyChantools(t *harnessTest) {
507+
chantools := NewChantoolsHarness(t.t)
508+
509+
// Use chantools to create a new wallet and derive a new key.
510+
chantools.CreateWallet(t.t)
511+
groupKeyXpub, groupKeyFingerprint := chantools.DeriveKey(t.t)
512+
513+
t.Logf("Extended public key (xpub): %v", groupKeyXpub)
514+
t.Logf("Master Fingerprint: %v", groupKeyFingerprint)
515+
516+
// Formulate the external group key which will be used in the asset
517+
// minting request.
518+
fingerPrintBytes, err := hex.DecodeString(groupKeyFingerprint)
519+
require.NoError(t.t, err)
520+
521+
externalGroupKey := &taprpc.ExternalKey{
522+
Xpub: groupKeyXpub,
523+
MasterFingerprint: fingerPrintBytes,
524+
DerivationPath: "m/86'/1'/0'/0/0",
525+
}
526+
527+
// Now we will use the external group key to mint.
528+
//
529+
// Construct external signer callback to sign the group virtual PSBT.
530+
signerCallback := func(
531+
unsealedAsset []*mintrpc.UnsealedAsset) []ExternalSigRes {
532+
533+
var res []ExternalSigRes
534+
for idx := range unsealedAsset {
535+
unsealedA := unsealedAsset[idx]
536+
537+
t.Logf("Unsigned group virtual PSBT: %v",
538+
unsealedA.GroupVirtualPsbt)
539+
540+
signedPsbt := chantools.SignPsbt(
541+
t.t, unsealedA.GroupVirtualPsbt,
542+
)
543+
t.Logf("Signed group virtual PSBT: %v", signedPsbt)
544+
545+
var assetID asset.ID
546+
copy(
547+
assetID[:],
548+
unsealedA.GroupKeyRequest.AnchorGenesis.AssetId,
549+
)
550+
551+
res = append(res, ExternalSigRes{
552+
SignedPsbt: signedPsbt,
553+
AssetID: assetID,
554+
})
555+
}
556+
557+
return res
558+
}
559+
560+
// Mint assets with the external signer.
561+
//
562+
// Add asset mint request to mint batch.
563+
mintReq := CopyRequest(issuableAssets[0])
564+
mintReq.Asset.ExternalGroupKey = externalGroupKey
565+
566+
assetReqs := []*mintrpc.MintAssetRequest{mintReq}
567+
568+
batchAssets := MintAssetExternalSigner(
569+
t, t.tapd, assetReqs, signerCallback,
570+
)
571+
t.Logf("First batch asset: %v", batchAssets)
572+
573+
// Formulate a second asset mint request with the same external group
574+
// key. This will ensure that we can mint two different tranches into
575+
// the same group.
576+
mintReq2 := CopyRequest(issuableAssets[0])
577+
mintReq2.Asset.Name = "itestbuxx-money-printer-brrr-tranche-2"
578+
mintReq2.Asset.ExternalGroupKey = externalGroupKey
579+
580+
assetReqs2 := []*mintrpc.MintAssetRequest{mintReq2}
581+
582+
// Mint assets with the external signer.
583+
batchAssets2 := MintAssetExternalSigner(
584+
t, t.tapd, assetReqs2, signerCallback,
585+
)
586+
t.Logf("Second batch asset: %v", batchAssets2)
587+
}
588+
504589
// Derive a random key on an LND node, with a key family not matching the
505590
// Taproot Assets key family.
506591
func deriveRandomKey(t *testing.T, ctxt context.Context,

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ var testCases = []*testCase{
3737
name: "mint fund seal assets",
3838
test: testMintFundSealAssets,
3939
},
40+
{
41+
name: "mint external group key chantools",
42+
test: testMintExternalGroupKeyChantools,
43+
},
4044
{
4145
name: "mint asset decimal display",
4246
test: testMintAssetWithDecimalDisplayMetaField,

itest/utils.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package itest
33
import (
44
"bytes"
55
"context"
6+
"fmt"
7+
"log"
68
"testing"
79
"time"
810

@@ -30,6 +32,7 @@ import (
3032
"github.com/lightningnetwork/lnd/lnrpc"
3133
"github.com/lightningnetwork/lnd/lntest/node"
3234
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
35+
"github.com/lightningnetwork/lnd/tlv"
3336
"github.com/stretchr/testify/require"
3437
"google.golang.org/grpc"
3538
"google.golang.org/protobuf/proto"
@@ -735,6 +738,151 @@ func ManualMintSimpleAsset(t *harnessTest, lndNode *node.HarnessNode,
735738
return mintedAsset[0], &importReq
736739
}
737740

741+
// ExternalSigRes is a helper struct that holds the signed PSBT and the
742+
// corresponding asset ID.
743+
type ExternalSigRes struct {
744+
SignedPsbt psbt.Packet
745+
AssetID asset.ID
746+
}
747+
748+
// ExternalSigCallback is a callback function that is called to sign the group
749+
// virtual PSBT with external signers.
750+
type ExternalSigCallback func([]*mintrpc.UnsealedAsset) []ExternalSigRes
751+
752+
// MintAssetExternalSigner is a helper function that mints a batch of assets and
753+
// calls the external signer callback to sign the group virtual PSBT.
754+
func MintAssetExternalSigner(t *harnessTest, tapNode *tapdHarness,
755+
assetReqs []*mintrpc.MintAssetRequest,
756+
externalSignerCallback ExternalSigCallback) []*taprpc.Asset {
757+
758+
BuildMintingBatch(t.t, tapNode, assetReqs)
759+
760+
// Fund mint batch with BTC.
761+
ctxb := context.Background()
762+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
763+
764+
fundResp, err := tapNode.FundBatch(ctxt, &mintrpc.FundBatchRequest{})
765+
require.NoError(t.t, err)
766+
767+
// Cancel the context for the fund request call.
768+
cancel()
769+
770+
require.NotEmpty(t.t, fundResp.Batch)
771+
require.Equal(
772+
t.t, mintrpc.BatchState_BATCH_STATE_PENDING,
773+
fundResp.Batch.Batch.State,
774+
)
775+
require.Len(t.t, fundResp.Batch.UnsealedAssets, 1)
776+
777+
// Pass unsealed assets to external signer callback to sign the group
778+
// virtual PSBT.
779+
callbackRes := externalSignerCallback(fundResp.Batch.UnsealedAssets)
780+
781+
// Extract group witness from signed PSBTs.
782+
var groupWitnesses []*taprpc.GroupWitness
783+
for idx := range callbackRes {
784+
res := callbackRes[idx]
785+
signedPsbt := res.SignedPsbt
786+
genesisAssetID := res.AssetID
787+
788+
// Sanity check signed PSBT.
789+
require.Len(t.t, signedPsbt.Inputs, 1)
790+
require.Len(t.t, signedPsbt.Outputs, 1)
791+
792+
// Extract witness from signed PSBT.
793+
witnessStack, err := DeserializeWitnessStack(
794+
signedPsbt.Inputs[0].FinalScriptWitness,
795+
)
796+
if err != nil {
797+
log.Fatalf("Failed to deserialize witness stack: %v",
798+
err)
799+
}
800+
801+
groupWitness := taprpc.GroupWitness{
802+
GenesisId: genesisAssetID[:],
803+
Witness: witnessStack,
804+
}
805+
806+
groupWitnesses = append(groupWitnesses, &groupWitness)
807+
}
808+
809+
// Seal the batch with the group witnesses.
810+
ctxt, cancel = context.WithTimeout(ctxb, defaultWaitTimeout)
811+
812+
sealReq := mintrpc.SealBatchRequest{
813+
GroupWitnesses: groupWitnesses,
814+
}
815+
sealResp, err := tapNode.SealBatch(ctxt, &sealReq)
816+
require.NoError(t.t, err)
817+
818+
// Cancel the context for the seal request call.
819+
cancel()
820+
821+
require.NotEmpty(t.t, sealResp.Batch)
822+
823+
// With the batch sealed successfully, we can now finalize it and
824+
// broadcast the anchor TX.
825+
ctxt, cancel = context.WithCancel(context.Background())
826+
defer cancel()
827+
stream, err := tapNode.SubscribeMintEvents(
828+
ctxt, &mintrpc.SubscribeMintEventsRequest{},
829+
)
830+
require.NoError(t.t, err)
831+
sub := &EventSubscription[*mintrpc.MintEvent]{
832+
ClientEventStream: stream,
833+
Cancel: cancel,
834+
}
835+
836+
batchTXID, batchKey := FinalizeBatchUnconfirmed(
837+
t.t, t.lndHarness.Miner().Client, tapNode, assetReqs,
838+
)
839+
batchAssets := ConfirmBatch(
840+
t.t, t.lndHarness.Miner().Client, tapNode, assetReqs, sub,
841+
batchTXID, batchKey,
842+
)
843+
844+
return batchAssets
845+
}
846+
847+
// DeserializeWitnessStack deserializes a serialized witness stack into a
848+
// [][]byte.
849+
//
850+
// TODO(ffranr): Reconcile this function with asset.TxWitnessDecoder.
851+
func DeserializeWitnessStack(serialized []byte) ([][]byte, error) {
852+
var (
853+
// buf is a general scratch buffer used when reading.
854+
buf [8]byte
855+
856+
stack [][]byte
857+
)
858+
reader := bytes.NewReader(serialized)
859+
860+
// Read the number of witness elements (compact size integer)
861+
count, err := tlv.ReadVarInt(reader, &buf)
862+
if err != nil {
863+
return nil, fmt.Errorf("failed to read witness count: %w", err)
864+
}
865+
866+
// Read each witness element
867+
for i := uint64(0); i < count; i++ {
868+
elementSize, err := tlv.ReadVarInt(reader, &buf)
869+
if err != nil {
870+
return nil, fmt.Errorf("failed to read witness "+
871+
"element size: %w", err)
872+
}
873+
874+
// Read the witness element data
875+
element := make([]byte, elementSize)
876+
if _, err := reader.Read(element); err != nil {
877+
return nil, fmt.Errorf("failed to read witness "+
878+
"element data: %w", err)
879+
}
880+
stack = append(stack, element)
881+
}
882+
883+
return stack, nil
884+
}
885+
738886
// SyncUniverses syncs the universes of two tapd instances and waits until they
739887
// are in sync.
740888
func SyncUniverses(ctx context.Context, t *testing.T, clientTapd,

0 commit comments

Comments
 (0)