From f3ec01d6a608110fdb0038f381e98803a39c0edd Mon Sep 17 00:00:00 2001 From: Jonathan Harvey-Buschel Date: Wed, 4 Oct 2023 13:47:18 +0200 Subject: [PATCH 1/5] multi: fix manual grouped asset creation in tests In this commit, we update multiple unit tests to not set an asset group key directly, and instead use some test helper that calls asset.New(). This ensures that the group witness is copied into the asset. The only time setting the group key directly is acceptable is before a group witness has been created, or in a minting request. --- asset/asset.go | 7 +-- asset/mock.go | 2 +- commitment/commitment_test.go | 93 +++++++++++++++++++---------------- go.mod | 2 +- tapdb/assets_store_test.go | 20 +++++--- tapscript/mint.go | 2 +- vm/vm_test.go | 7 +-- 7 files changed, 75 insertions(+), 58 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 4c016bd4a..bc95158ad 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -1208,9 +1208,10 @@ func (a *Asset) Copy() *Asset { if a.GroupKey != nil { assetCopy.GroupKey = &GroupKey{ - RawKey: a.GroupKey.RawKey, - GroupPubKey: a.GroupKey.GroupPubKey, - Witness: a.GroupKey.Witness, + RawKey: a.GroupKey.RawKey, + GroupPubKey: a.GroupKey.GroupPubKey, + TapscriptRoot: a.GroupKey.TapscriptRoot, + Witness: a.GroupKey.Witness, } } diff --git a/asset/mock.go b/asset/mock.go index 8772d5842..7f32026de 100644 --- a/asset/mock.go +++ b/asset/mock.go @@ -175,7 +175,7 @@ func (m *MockGroupTxBuilder) BuildGenesisTx(newAsset *Asset) (*wire.MsgTx, // First, we check that the passed asset is a genesis grouped asset // that has no group witness. if !newAsset.NeedsGenesisWitnessForGroup() { - return nil, nil, fmt.Errorf("asset is not a genesis grouped" + + return nil, nil, fmt.Errorf("asset is not a genesis grouped " + "asset") } diff --git a/commitment/commitment_test.go b/commitment/commitment_test.go index 53da735bc..8d456abd8 100644 --- a/commitment/commitment_test.go +++ b/commitment/commitment_test.go @@ -72,39 +72,44 @@ func TestNewAssetCommitment(t *testing.T) { t.Parallel() genesis1 := asset.RandGenesis(t, asset.Normal) + genesis2 := asset.RandGenesis(t, asset.Normal) genesis1Collectible := asset.RandGenesis(t, asset.Collectible) - genesis1CollectibleScriptKey := asset.RandScriptKey(t) genesis1CollectibleProtoAsset := asset.NewAssetNoErr( - t, genesis1Collectible, 1, 0, 0, genesis1CollectibleScriptKey, - nil, - ) - genesis2 := asset.RandGenesis(t, asset.Normal) - genesis2ProtoAsset := asset.RandAssetWithValues( - t, genesis2, nil, asset.RandScriptKey(t), + t, genesis1Collectible, 1, 0, 0, asset.RandScriptKey(t), nil, ) group1Anchor := randAsset(t, genesis1, nil) groupKey1, group1PrivBytes := asset.RandGroupKeyWithSigner( t, genesis1, group1Anchor, ) - group1Priv, group1Pub := btcec.PrivKeyFromBytes(group1PrivBytes) - group1Anchor.GroupKey = groupKey1 + group1Anchor = asset.NewAssetNoErr( + t, genesis1, group1Anchor.Amount, group1Anchor.LockTime, + group1Anchor.RelativeLockTime, group1Anchor.ScriptKey, + groupKey1, + ) groupKey1Collectible := asset.RandGroupKey( t, genesis1Collectible, genesis1CollectibleProtoAsset, ) + genesis2ProtoAsset := randAsset(t, genesis2, nil) groupKey2 := asset.RandGroupKey(t, genesis2, genesis2ProtoAsset) copyOfGroupKey1Collectible := &asset.GroupKey{ - RawKey: groupKey1Collectible.RawKey, - GroupPubKey: groupKey1Collectible.GroupPubKey, - Witness: groupKey1Collectible.Witness, + RawKey: groupKey1Collectible.RawKey, + GroupPubKey: groupKey1Collectible.GroupPubKey, + TapscriptRoot: groupKey1Collectible.TapscriptRoot, + Witness: groupKey1Collectible.Witness, } group1Reissued := randAsset(t, genesis2, nil) genTxBuilder := asset.MockGroupTxBuilder{} + group1Priv, group1Pub := btcec.PrivKeyFromBytes(group1PrivBytes) group1ReissuedGroupKey, err := asset.DeriveGroupKey( asset.NewMockGenesisSigner(group1Priv), &genTxBuilder, test.PubToKeyDesc(group1Pub), genesis1, genesis2ProtoAsset, ) require.NoError(t, err) - group1Reissued.GroupKey = group1ReissuedGroupKey + group1Reissued = asset.NewAssetNoErr( + t, genesis2, group1Reissued.Amount, group1Reissued.LockTime, + group1Reissued.RelativeLockTime, group1Reissued.ScriptKey, + group1ReissuedGroupKey, + ) testCases := []struct { name string @@ -503,10 +508,6 @@ func TestSplitCommitment(t *testing.T) { groupKeyCollectible := asset.RandGroupKey( t, genesisCollectible, collectibleProtoAsset, ) - normalInputAsset := normalProtoAsset.Copy() - normalInputAsset.GroupKey = groupKeyNormal - collectibleInputAsset := collectibleProtoAsset.Copy() - collectibleInputAsset.GroupKey = groupKeyCollectible testCases := []struct { name string @@ -898,7 +899,10 @@ func TestTapCommitmentKeyPopulation(t *testing.T) { var groupKey *asset.GroupKey if assetDesc.HasGroupKey { groupKey = asset.RandGroupKey(t, genesis, a) - a.GroupKey = groupKey + a = asset.NewAssetNoErr( + t, genesis, a.Amount, a.LockTime, + a.RelativeLockTime, a.ScriptKey, groupKey, + ) } commitment, err := NewAssetCommitment(a) @@ -932,18 +936,26 @@ func TestUpdateAssetCommitment(t *testing.T) { groupKey1, group1PrivBytes := asset.RandGroupKeyWithSigner( t, genesis1, group1Anchor, ) - group1Priv, group1Pub := btcec.PrivKeyFromBytes(group1PrivBytes) - group1Anchor.GroupKey = groupKey1 + group1Anchor = asset.NewAssetNoErr( + t, genesis1, group1Anchor.Amount, group1Anchor.LockTime, + group1Anchor.RelativeLockTime, group1Anchor.ScriptKey, + groupKey1, + ) group2Anchor := randAsset(t, genesis2, nil) groupKey2 := asset.RandGroupKey(t, genesis2, group2Anchor) - group1Reissued := group2Anchor.Copy() + group1Reissued := randAsset(t, genesis2, nil) genTxBuilder := asset.MockGroupTxBuilder{} + group1Priv, group1Pub := btcec.PrivKeyFromBytes(group1PrivBytes) group1ReissuedGroupKey, err := asset.DeriveGroupKey( asset.NewMockGenesisSigner(group1Priv), &genTxBuilder, test.PubToKeyDesc(group1Pub), genesis1, group1Reissued, ) require.NoError(t, err) - group1Reissued.GroupKey = group1ReissuedGroupKey + group1Reissued = asset.NewAssetNoErr( + t, genesis2, group1Reissued.Amount, group1Reissued.LockTime, + group1Reissued.RelativeLockTime, group1Reissued.ScriptKey, + group1ReissuedGroupKey, + ) assetNoGroup := randAsset(t, genesis2, nil) copyOfAssetNoGroup := assetNoGroup.Copy() @@ -990,19 +1002,6 @@ func TestUpdateAssetCommitment(t *testing.T) { { name: "insertion of asset with group key", f: func() (*asset.Asset, error) { - group1Reissued := randAsset(t, genesis2, nil) - genTxBuilder := asset.MockGroupTxBuilder{} - gen2ProtoAsset := asset.RandAssetWithValues( - t, genesis2, nil, asset.RandScriptKey(t), - ) - group1ReissuedGroupKey, err := asset.DeriveGroupKey( - asset.NewMockGenesisSigner(group1Priv), - &genTxBuilder, - test.PubToKeyDesc(group1Priv.PubKey()), - genesis1, gen2ProtoAsset, - ) - require.NoError(t, err) - group1Reissued.GroupKey = group1ReissuedGroupKey return group1Reissued, groupAssetCommitment.Upsert(group1Reissued) }, @@ -1088,11 +1087,15 @@ func TestUpdateTapCommitment(t *testing.T) { genesis3 := asset.RandGenesis(t, asset.Normal) asset3 := randAsset(t, genesis3, groupKey1) - asset1 := protoAsset1.Copy() - asset1.GroupKey = groupKey1 + asset1 := asset.NewAssetNoErr( + t, genesis1, protoAsset1.Amount, protoAsset1.LockTime, + protoAsset1.RelativeLockTime, protoAsset1.ScriptKey, groupKey1, + ) - asset2 := protoAsset2.Copy() - asset2.GroupKey = groupKey2 + asset2 := asset.NewAssetNoErr( + t, genesis2, protoAsset2.Amount, protoAsset2.LockTime, + protoAsset2.RelativeLockTime, protoAsset2.ScriptKey, groupKey2, + ) assetCommitment1, err := NewAssetCommitment(asset1) require.NoError(t, err) @@ -1260,14 +1263,18 @@ func TestTapCommitmentDeepCopy(t *testing.T) { genesis1 := asset.RandGenesis(t, asset.Normal) protoAsset1 := randAsset(t, genesis1, nil) groupKey1 := asset.RandGroupKey(t, genesis1, protoAsset1) - asset1 := protoAsset1.Copy() - asset1.GroupKey = groupKey1 + asset1 := asset.NewAssetNoErr( + t, genesis1, protoAsset1.Amount, protoAsset1.LockTime, + protoAsset1.RelativeLockTime, protoAsset1.ScriptKey, groupKey1, + ) genesis2 := asset.RandGenesis(t, asset.Normal) protoAsset2 := randAsset(t, genesis2, nil) groupKey2 := asset.RandGroupKey(t, genesis2, protoAsset2) - asset2 := protoAsset2.Copy() - asset2.GroupKey = groupKey2 + asset2 := asset.NewAssetNoErr( + t, genesis2, protoAsset2.Amount, protoAsset2.LockTime, + protoAsset2.RelativeLockTime, protoAsset2.ScriptKey, groupKey2, + ) assetCommitment1, err := NewAssetCommitment(asset1) require.NoError(t, err) diff --git a/go.mod b/go.mod index d4a64de56..51ae322b6 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/btcsuite/btcwallet v0.16.10-0.20231017144732-e3ff37491e9c github.com/caddyserver/certmagic v0.17.2 github.com/davecgh/go-spew v1.1.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/go-errors/errors v1.0.1 github.com/golang-migrate/migrate/v4 v4.16.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 @@ -74,7 +75,6 @@ require ( github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/lru v1.0.0 // indirect github.com/docker/cli v20.10.17+incompatible // indirect github.com/docker/docker v20.10.24+incompatible // indirect diff --git a/tapdb/assets_store_test.go b/tapdb/assets_store_test.go index 82c607300..7cf5d3c01 100644 --- a/tapdb/assets_store_test.go +++ b/tapdb/assets_store_test.go @@ -145,7 +145,7 @@ func randAsset(t *testing.T, genOpts ...assetGenOpt) *asset.Asset { protoAsset = asset.NewAssetNoErr( t, genesis, opts.amt, lockTime, relativeLockTime, - opts.scriptKey, nil, + opts.scriptKey, nil, asset.WithAssetVersion(opts.version), ) if opts.groupAnchorGen != nil { @@ -167,6 +167,11 @@ func randAsset(t *testing.T, genOpts ...assetGenOpt) *asset.Asset { ScriptKey: opts.scriptKey, } + // Go with an even amount to make the splits always work nicely. + if newAsset.Amount%2 != 0 { + newAsset.Amount++ + } + // 50/50 chance that we'll actually have a group key. Or we'll always // use it if a custom group key was specified. switch { @@ -174,12 +179,15 @@ func randAsset(t *testing.T, genOpts ...assetGenOpt) *asset.Asset { break case opts.customGroup || test.RandInt[int]()%2 == 0: - newAsset.GroupKey = assetGroupKey - } + // If we're using a group key, we want to leave the asset with + // the group witness and not a random witness. + assetWithGroup := asset.NewAssetNoErr( + t, genesis, newAsset.Amount, newAsset.LockTime, + newAsset.RelativeLockTime, newAsset.ScriptKey, + assetGroupKey, asset.WithAssetVersion(opts.version), + ) - // Go with an even amount to make the splits always work nicely. - if newAsset.Amount%2 != 0 { - newAsset.Amount++ + return assetWithGroup } // For the witnesses, we'll flip a coin: we'll either make a genesis diff --git a/tapscript/mint.go b/tapscript/mint.go index 7eee733e3..66d768b15 100644 --- a/tapscript/mint.go +++ b/tapscript/mint.go @@ -20,7 +20,7 @@ func BuildGenesisTx(newAsset *asset.Asset) (*wire.MsgTx, // First, we check that the passed asset is a genesis grouped asset // that has no group witness. if !newAsset.NeedsGenesisWitnessForGroup() { - return nil, nil, fmt.Errorf("asset is not a genesis grouped" + + return nil, nil, fmt.Errorf("asset is not a genesis grouped " + "asset") } diff --git a/vm/vm_test.go b/vm/vm_test.go index 1e5d7e708..c478e4480 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -54,9 +54,10 @@ func randAsset(t *testing.T, assetType asset.Type, protoAsset := asset.RandAssetWithValues(t, genesis, nil, scriptKey) groupKey := asset.RandGroupKey(t, genesis, protoAsset) - fullAsset := protoAsset.Copy() - fullAsset.GroupKey = groupKey - return fullAsset + return asset.NewAssetNoErr( + t, genesis, protoAsset.Amount, protoAsset.LockTime, + protoAsset.RelativeLockTime, scriptKey, groupKey, + ) } func genTaprootKeySpend(t *testing.T, privKey btcec.PrivateKey, From 2efdfb94962c5f4de5276e935d264d8755027cad Mon Sep 17 00:00:00 2001 From: Jonathan Harvey-Buschel Date: Mon, 9 Oct 2023 22:32:00 +0200 Subject: [PATCH 2/5] asset: test genesis detection, group key creation --- asset/asset.go | 6 +- asset/asset_test.go | 172 +++++++++++++++++++++++++++++++++++++++++++- asset/mock.go | 10 +++ 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index bc95158ad..6a247de1f 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -513,7 +513,7 @@ type GroupKeyReveal struct { func (g *GroupKeyReveal) GroupPubKey(assetID ID) (*btcec.PublicKey, error) { rawKey, err := g.RawKey.ToPubKey() if err != nil { - return nil, err + return nil, fmt.Errorf("group reveal raw key invalid: %w", err) } return GroupPubKey(rawKey, assetID[:], g.TapscriptRoot) @@ -814,6 +814,10 @@ func DeriveGroupKey(genSigner GenesisSigner, genBuilder GenesisTxBuilder, return nil, fmt.Errorf("asset is not a genesis asset") } + if newAsset.GroupKey != nil { + return nil, fmt.Errorf("asset already has group key") + } + if initialGen.Type != newAsset.Type { return nil, fmt.Errorf("asset group type mismatch") } diff --git a/asset/asset_test.go b/asset/asset_test.go index 5cd669419..4e0232086 100644 --- a/asset/asset_test.go +++ b/asset/asset_test.go @@ -231,6 +231,102 @@ func TestGroupKeyIsEqual(t *testing.T) { } } +// TestGenesisAssetClassification tests that the multiple forms of genesis asset +// are recognized correctly. +func TestGenesisAssetClassification(t *testing.T) { + t.Parallel() + + baseGen := RandGenesis(t, Normal) + baseScriptKey := RandScriptKey(t) + baseAsset := RandAssetWithValues(t, baseGen, nil, baseScriptKey) + assetValidGroup := RandAsset(t, Collectible) + assetNeedsWitness := baseAsset.Copy() + assetNeedsWitness.GroupKey = &GroupKey{ + GroupPubKey: *test.RandPubKey(t), + } + nonGenAsset := baseAsset.Copy() + nonGenAsset.PrevWitnesses = []Witness{{ + PrevID: &PrevID{ + OutPoint: wire.OutPoint{ + Hash: hashBytes1, + Index: 1, + }, + ID: hashBytes1, + ScriptKey: ToSerialized(pubKey), + }, + TxWitness: sigWitness, + SplitCommitment: nil, + }} + groupMemberNonGen := nonGenAsset.Copy() + groupMemberNonGen.GroupKey = &GroupKey{ + GroupPubKey: *test.RandPubKey(t), + } + splitAsset := nonGenAsset.Copy() + splitAsset.PrevWitnesses[0].TxWitness = nil + splitAsset.PrevWitnesses[0].SplitCommitment = &SplitCommitment{} + + tests := []struct { + name string + genAsset *Asset + isGenesis, needsWitness, hasWitness bool + }{ + { + name: "group anchor with witness", + genAsset: assetValidGroup, + isGenesis: false, + needsWitness: false, + hasWitness: true, + }, + { + name: "ungrouped genesis asset", + genAsset: baseAsset, + isGenesis: true, + needsWitness: false, + hasWitness: false, + }, + { + name: "group anchor without witness", + genAsset: assetNeedsWitness, + isGenesis: true, + needsWitness: true, + hasWitness: false, + }, + { + name: "non-genesis asset", + genAsset: nonGenAsset, + isGenesis: false, + needsWitness: false, + hasWitness: false, + }, + { + name: "non-genesis grouped asset", + genAsset: groupMemberNonGen, + isGenesis: false, + needsWitness: false, + hasWitness: false, + }, + { + name: "split asset", + genAsset: splitAsset, + isGenesis: false, + needsWitness: false, + hasWitness: false, + }, + } + + for _, testCase := range tests { + testCase := testCase + a := testCase.genAsset + + hasGenWitness := a.HasGenesisWitness() + require.Equal(t, testCase.isGenesis, hasGenWitness) + needsGroupWitness := a.NeedsGenesisWitnessForGroup() + require.Equal(t, testCase.needsWitness, needsGroupWitness) + hasGroupWitness := a.HasGenesisWitnessForGroup() + require.Equal(t, testCase.hasWitness, hasGroupWitness) + } +} + // TestValidateAssetName tests that asset names are validated correctly. func TestValidateAssetName(t *testing.T) { t.Parallel() @@ -510,11 +606,12 @@ func TestAssetGroupKey(t *testing.T) { t.Parallel() privKey, err := btcec.NewPrivateKey() + groupPub := privKey.PubKey() require.NoError(t, err) privKeyCopy := btcec.PrivKeyFromScalar(&privKey.Key) genSigner := NewMockGenesisSigner(privKeyCopy) genBuilder := MockGroupTxBuilder{} - fakeKeyDesc := test.PubToKeyDesc(privKeyCopy.PubKey()) + fakeKeyDesc := test.PubToKeyDesc(groupPub) fakeScriptKey := NewScriptKeyBip86(fakeKeyDesc) g := Genesis{ @@ -544,6 +641,79 @@ func TestAssetGroupKey(t *testing.T) { t, schnorr.SerializePubKey(tweakedKey.PubKey()), schnorr.SerializePubKey(&keyGroup.GroupPubKey), ) + + // Group key tweaking should fail when given invalid tweaks. + badTweak := test.RandBytes(33) + _, err = GroupPubKey(groupPub, badTweak, badTweak) + require.Error(t, err) + + _, err = GroupPubKey(groupPub, groupTweak[:], badTweak) + require.Error(t, err) +} + +// TestDeriveGroupKey tests that group key derivation fails for assets that are +// not eligible to be group anchors. +func TestDeriveGroupKey(t *testing.T) { + t.Parallel() + + groupPriv := test.RandPrivKey(t) + groupPub := groupPriv.PubKey() + groupKeyDesc := test.PubToKeyDesc(groupPub) + genSigner := NewMockGenesisSigner(groupPriv) + genBuilder := MockGroupTxBuilder{} + + baseGen := RandGenesis(t, Normal) + collectGen := RandGenesis(t, Collectible) + baseScriptKey := RandScriptKey(t) + protoAsset := RandAssetWithValues(t, baseGen, nil, baseScriptKey) + nonGenProtoAsset := protoAsset.Copy() + nonGenProtoAsset.PrevWitnesses = []Witness{{ + PrevID: &PrevID{ + OutPoint: wire.OutPoint{ + Hash: hashBytes1, + Index: 1, + }, + ID: hashBytes1, + ScriptKey: ToSerialized(pubKey), + }, + TxWitness: sigWitness, + SplitCommitment: nil, + }} + groupedProtoAsset := protoAsset.Copy() + groupedProtoAsset.GroupKey = &GroupKey{ + GroupPubKey: *groupPub, + } + + // A prototype asset is required for building the genesis virtual TX. + _, err := DeriveGroupKey( + genSigner, &genBuilder, groupKeyDesc, baseGen, nil, + ) + require.Error(t, err) + + // The prototype asset must have a genesis witness. + _, err = DeriveGroupKey( + genSigner, &genBuilder, groupKeyDesc, baseGen, nonGenProtoAsset, + ) + require.Error(t, err) + + // The prototype asset must not have a group key set. + _, err = DeriveGroupKey( + genSigner, &genBuilder, groupKeyDesc, baseGen, groupedProtoAsset, + ) + require.Error(t, err) + + // The anchor genesis used for signing must have the same asset type + // as the prototype asset being signed. + _, err = DeriveGroupKey( + genSigner, &genBuilder, groupKeyDesc, collectGen, protoAsset, + ) + require.Error(t, err) + + groupKey, err := DeriveGroupKey( + genSigner, &genBuilder, groupKeyDesc, baseGen, protoAsset, + ) + require.NoError(t, err) + require.NotNil(t, groupKey) } // TestAssetWitness tests that the asset group witness can be serialized and diff --git a/asset/mock.go b/asset/mock.go index 7f32026de..4684e585a 100644 --- a/asset/mock.go +++ b/asset/mock.go @@ -323,6 +323,16 @@ func RandID(t testing.TB) ID { return a } +// RandAssetType creates a random asset type. +func RandAssetType(t testing.TB) Type { + isCollectible := test.RandBool() + if isCollectible { + return Collectible + } + + return Normal +} + // NewAssetNoErr creates an asset and fails the test if asset creation fails. func NewAssetNoErr(t testing.TB, gen Genesis, amt, locktime, relocktime uint64, scriptKey ScriptKey, groupKey *GroupKey, opts ...NewAssetOpt) *Asset { From 2a30f86307959014ef36aca9986db2d8a494060d Mon Sep 17 00:00:00 2001 From: Jonathan Harvey-Buschel Date: Tue, 10 Oct 2023 02:11:19 +0200 Subject: [PATCH 3/5] tapdb: use group verifiers in tests --- tapdb/asset_minting_test.go | 45 +++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/tapdb/asset_minting_test.go b/tapdb/asset_minting_test.go index d3486abc0..0698902ce 100644 --- a/tapdb/asset_minting_test.go +++ b/tapdb/asset_minting_test.go @@ -421,11 +421,9 @@ func seedlingsToAssetRoot(t *testing.T, genesisPoint wire.OutPoint, ) require.NoError(t, err) - // Finally make a new asset commitment (the inner SMT tree) for + // Finally, make a new asset commitment (the inner SMT tree) for // this newly created asset. - assetRoot, err := commitment.NewAssetCommitment( - newAsset, - ) + assetRoot, err := commitment.NewAssetCommitment(newAsset) require.NoError(t, err) assetRoots = append(assetRoots, assetRoot) @@ -1059,6 +1057,9 @@ func TestGroupAnchors(t *testing.T) { ctx := context.Background() const numSeedlings = 10 assetStore, _, _ := newAssetStore(t) + groupVerifier := tapgarden.GenGroupVerifier(ctx, assetStore) + groupAnchorVerifier := tapgarden.GenGroupAnchorVerifier(ctx, assetStore) + rawGroupAnchorVerifier := tapgarden.GenRawGroupAnchorVerifier(ctx) // First, we'll write a new minting batch to disk, including an // internal key and a set of seedlings. One random seedling will @@ -1113,6 +1114,19 @@ func TestGroupAnchors(t *testing.T) { ) seedlings[secondGrouped].GroupAnchor = &secondAnchor + // Record the number of seedlings set as group anchors and members. + // These counts should not change after sprouting. + batchSeedlings := maps.Values(mintingBatch.Seedlings) + isGroupAnchor := func(s *tapgarden.Seedling) bool { + return s.EnableEmission == true + } + isGroupMember := func(s *tapgarden.Seedling) bool { + return s.GroupAnchor != nil || s.GroupInfo != nil + } + + anchorCount := fn.Count(batchSeedlings, isGroupAnchor) + memberCount := fn.Count(batchSeedlings, isGroupMember) + // Now we'll map these seedlings to an asset commitment and insert them // into the DB as sprouts. genesisPacket := randGenesisPacket(t) @@ -1135,6 +1149,29 @@ func TestGroupAnchors(t *testing.T) { assertBatchState(t, mintingBatches[0], tapgarden.BatchStateCommitted) assertPsbtEqual(t, genesisPacket, mintingBatches[0].GenesisPacket) assertAssetsEqual(t, assetRoot, mintingBatches[0].RootAssetCommitment) + + // Check that the number of group anchors and members matches the batch + // state when frozen. + storedAssets := mintingBatches[0].RootAssetCommitment.CommittedAssets() + groupedAssets := fn.Filter(storedAssets, func(a *asset.Asset) bool { + return a.GroupKey != nil + }) + require.Equal(t, anchorCount+memberCount, len(groupedAssets)) + require.True(t, fn.All(groupedAssets, func(a *asset.Asset) bool { + return groupVerifier(&a.GroupKey.GroupPubKey) == nil + })) + + // Both group anchor verifiers must return the same result. + groupAnchors := fn.Filter(groupedAssets, func(a *asset.Asset) bool { + return groupAnchorVerifier(&a.Genesis, a.GroupKey) == nil + }) + require.Equal(t, anchorCount, len(groupAnchors)) + + rawGroupAnchors := fn.Filter(groupAnchors, func(a *asset.Asset) bool { + return rawGroupAnchorVerifier(&a.Genesis, a.GroupKey) == nil + }) + require.Equal(t, anchorCount, len(rawGroupAnchors)) + require.Equal(t, groupAnchors, rawGroupAnchors) } func init() { From b56f72eea88ffa39d156e8adc74ee8ffcbd78563 Mon Sep 17 00:00:00 2001 From: Jonathan Harvey-Buschel Date: Wed, 18 Oct 2023 00:12:22 -0400 Subject: [PATCH 4/5] tapdb: test storage of multi-element witnesses --- tapdb/assets_store_test.go | 82 ++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/tapdb/assets_store_test.go b/tapdb/assets_store_test.go index 7cf5d3c01..331ea11c1 100644 --- a/tapdb/assets_store_test.go +++ b/tapdb/assets_store_test.go @@ -1424,9 +1424,9 @@ func TestAssetExportLog(t *testing.T) { require.Equal(t, 0, len(parcels)) } -// TestAssetGroupSigUpsert tests that if you try to insert another asset -// group sig with the same asset_gen_id, then only one is actually created. -func TestAssetGroupSigUpsert(t *testing.T) { +// TestAssetGroupWitnessUpsert tests that if you try to insert another asset +// group witness with the same asset_gen_id, then only one is actually created. +func TestAssetGroupWitnessUpsert(t *testing.T) { t.Parallel() _, _, db := newAssetStore(t) @@ -1435,7 +1435,7 @@ func TestAssetGroupSigUpsert(t *testing.T) { internalKey := test.RandPubKey(t) // First, we'll insert all the required rows we need to satisfy the - // foreign key constraints needed to insert a new genesis sig. + // foreign key constraints needed to insert a new genesis witness. keyID, err := db.UpsertInternalKey(ctx, InternalKey{ RawKey: internalKey.SerializeCompressed(), }) @@ -1457,24 +1457,74 @@ func TestAssetGroupSigUpsert(t *testing.T) { require.NoError(t, err) // With all the other items inserted, we'll now insert an asset group - // sig. - groupSigID, err := db.UpsertAssetGroupWitness(ctx, AssetGroupWitness{ - WitnessStack: []byte{0x01}, - GenAssetID: genAssetID, - GroupKeyID: groupID, - }) + // witness. + groupWitnessID, err := db.UpsertAssetGroupWitness( + ctx, AssetGroupWitness{ + WitnessStack: []byte{0x01}, + GenAssetID: genAssetID, + GroupKeyID: groupID, + }) require.NoError(t, err) // If we insert the very same sig, then we should get the same group sig // ID back. - groupSigID2, err := db.UpsertAssetGroupWitness(ctx, AssetGroupWitness{ - WitnessStack: []byte{0x01}, - GenAssetID: genAssetID, - GroupKeyID: groupID, - }) + groupWitnessID2, err := db.UpsertAssetGroupWitness( + ctx, AssetGroupWitness{ + WitnessStack: []byte{0x01}, + GenAssetID: genAssetID, + GroupKeyID: groupID, + }) + require.NoError(t, err) + + require.Equal(t, groupWitnessID, groupWitnessID2) +} + +// TestAssetGroupComplexWitness tests that we can store and load an asset group +// witness of multiple elements. +func TestAssetGroupComplexWitness(t *testing.T) { + t.Parallel() + + mintingStore, assetStore, db := newAssetStore(t) + ctx := context.Background() + + internalKey := test.RandPubKey(t) + groupAnchorGen := asset.RandGenesis(t, asset.RandAssetType(t)) + groupAnchorGen.MetaHash = [32]byte{} + tapscriptRoot := test.RandBytes(32) + groupSig := test.RandBytes(64) + + // First, we'll insert all the required rows we need to satisfy the + // foreign key constraints needed to insert a new genesis witness. + genesisPointID, err := upsertGenesisPoint( + ctx, db, groupAnchorGen.FirstPrevOut, + ) + require.NoError(t, err) + + genAssetID, err := upsertGenesis(ctx, db, genesisPointID, groupAnchorGen) + require.NoError(t, err) + + groupKey := asset.GroupKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: internalKey, + }, + GroupPubKey: *internalKey, + TapscriptRoot: tapscriptRoot, + Witness: fn.MakeSlice(tapscriptRoot, groupSig), + } + + _, err = upsertGroupKey( + ctx, &groupKey, assetStore.db, genesisPointID, genAssetID, + ) + require.NoError(t, err) + + // If we fetch the group, it should have all the fields correctly + // populated. + + storedGroup, err := mintingStore.FetchGroupByGroupKey(ctx, internalKey) require.NoError(t, err) - require.Equal(t, groupSigID, groupSigID2) + require.Equal(t, groupAnchorGen, *storedGroup.Genesis) + require.True(t, groupKey.IsEqual(storedGroup.GroupKey)) } // TestAssetGroupKeyUpsert tests that if you try to insert another asset group From 8f770907c637c09751c30a6f46d51aa1f69f69cd Mon Sep 17 00:00:00 2001 From: Jonathan Harvey-Buschel Date: Mon, 4 Dec 2023 18:34:20 -0500 Subject: [PATCH 5/5] proof: test genesis, group key reveal verification --- proof/append_test.go | 2 +- proof/proof.go | 8 +-- proof/proof_test.go | 131 ++++++++++++++++++++++++++++++++++++------- proof/verifier.go | 28 +++------ 4 files changed, 122 insertions(+), 47 deletions(-) diff --git a/proof/append_test.go b/proof/append_test.go index 1a1840a14..8a4ef2bb9 100644 --- a/proof/append_test.go +++ b/proof/append_test.go @@ -110,7 +110,7 @@ func runAppendTransitionTest(t *testing.T, assetType asset.Type, amt uint64, // Start with a minted genesis asset. genesisProof, senderPrivKey := genRandomGenesisWithProof( - t, assetType, &amt, nil, true, nil, nil, assetVersion, + t, assetType, &amt, nil, true, nil, nil, nil, nil, assetVersion, ) genesisBlob, err := EncodeAsProofFile(&genesisProof) require.NoError(t, err) diff --git a/proof/proof.go b/proof/proof.go index cb1192506..4d04ba68f 100644 --- a/proof/proof.go +++ b/proof/proof.go @@ -56,7 +56,7 @@ var ( // proof for a genesis asset has a non-zero meta hash, but doesn't have // a meta reveal. ErrGenesisRevealMetaRevealRequired = errors.New("genesis meta reveal " + - "reveal required") + "required") // ErrGenesisRevealMetaHashMismatch is an error returned if an asset // proof for a genesis asset has a genesis reveal where the meta hash @@ -70,12 +70,6 @@ var ( ErrGenesisRevealOutputIndexMismatch = errors.New("genesis reveal " + "output index mismatch") - // ErrGenesisRevealTypeMismatch is an error returned if an asset proof - // for a genesis asset has a genesis reveal where the asset type doesn't - // match the proof TLV field. - ErrGenesisRevealTypeMismatch = errors.New("genesis reveal type " + - "mismatch") - // ErrNonGenesisAssetWithGroupKeyReveal is an error returned if an asset // proof for a non-genesis asset contains a group key reveal. ErrNonGenesisAssetWithGroupKeyReveal = errors.New("non genesis asset " + diff --git a/proof/proof_test.go b/proof/proof_test.go index 671bedcb1..24b9bf66e 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -20,6 +20,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/internal/test" @@ -338,8 +339,9 @@ func TestProofEncoding(t *testing.T) { func genRandomGenesisWithProof(t testing.TB, assetType asset.Type, amt *uint64, tapscriptPreimage *commitment.TapscriptPreimage, - noMetaHash bool, metaReveal *MetaReveal, - genesisMutator genMutator, + noMetaHash bool, metaReveal *MetaReveal, genesisMutator genMutator, + genesisRevealMutator genRevealMutator, + groupRevealMutator groupRevealMutator, assetVersion asset.Version) (Proof, *btcec.PrivateKey) { t.Helper() @@ -372,13 +374,17 @@ func genRandomGenesisWithProof(t testing.TB, assetType asset.Type, asset.WithAssetVersion(assetVersion), ) assetGroupKey := asset.RandGroupKey(t, assetGenesis, protoAsset) - groupKeyReveal := asset.GroupKeyReveal{ + groupKeyReveal := &asset.GroupKeyReveal{ RawKey: asset.ToSerialized( assetGroupKey.RawKey.PubKey, ), TapscriptRoot: assetGroupKey.TapscriptRoot, } + if groupRevealMutator != nil { + groupRevealMutator(groupKeyReveal) + } + tapCommitment, assets, err := commitment.Mint( assetGenesis, assetGroupKey, &commitment.AssetDetails{ Type: assetType, @@ -436,6 +442,11 @@ func genRandomGenesisWithProof(t testing.TB, assetType asset.Type, txMerkleProof, err := NewTxMerkleProof([]*wire.MsgTx{genesisTx}, 0) require.NoError(t, err) + genReveal := &assetGenesis + if genesisRevealMutator != nil { + genReveal = genesisRevealMutator(genReveal) + } + return Proof{ PrevOut: assetGenesis.FirstPrevOut, BlockHeader: *blockHeader, @@ -455,13 +466,17 @@ func genRandomGenesisWithProof(t testing.TB, assetType asset.Type, MetaReveal: metaReveal, ExclusionProofs: nil, AdditionalInputs: nil, - GenesisReveal: &assetGenesis, - GroupKeyReveal: &groupKeyReveal, + GenesisReveal: genReveal, + GroupKeyReveal: groupKeyReveal, }, genesisPrivKey } type genMutator func(*asset.Genesis) +type groupRevealMutator func(*asset.GroupKeyReveal) + +type genRevealMutator func(*asset.Genesis) *asset.Genesis + func TestGenesisProofVerification(t *testing.T) { t.Parallel() @@ -476,15 +491,18 @@ func TestGenesisProofVerification(t *testing.T) { amount := uint64(5000) testCases := []struct { - name string - assetType asset.Type - amount *uint64 - assetVersion asset.Version - tapscriptPreimage *commitment.TapscriptPreimage - metaReveal *MetaReveal - noMetaHash bool - genesisMutator genMutator - expectedErr error + name string + assetType asset.Type + amount *uint64 + assetVersion asset.Version + tapscriptPreimage *commitment.TapscriptPreimage + metaReveal *MetaReveal + noMetaHash bool + noGroup bool + genesisMutator genMutator + genesisRevealMutator genRevealMutator + groupRevealMutator groupRevealMutator + expectedErr error }{ { name: "collectible genesis", @@ -584,6 +602,78 @@ func TestGenesisProofVerification(t *testing.T) { assetType: asset.Collectible, expectedErr: ErrGenesisRevealMetaRevealRequired, }, + { + name: "missing genesis reveal", + assetType: asset.Collectible, + noMetaHash: true, + genesisRevealMutator: func( + g *asset.Genesis) *asset.Genesis { + + return nil + }, + expectedErr: ErrGenesisRevealRequired, + }, + { + name: "genesis reveal asset ID mismatch", + assetType: asset.Normal, + amount: &amount, + noMetaHash: true, + genesisRevealMutator: func( + g *asset.Genesis) *asset.Genesis { + + gCopy := *g + gCopy.Tag += "mismatch" + return &gCopy + }, + expectedErr: ErrGenesisRevealAssetIDMismatch, + }, + { + name: "genesis reveal prev out mismatch", + assetType: asset.Collectible, + genesisRevealMutator: func( + g *asset.Genesis) *asset.Genesis { + + gCopy := *g + gCopy.FirstPrevOut = test.RandOp(t) + return &gCopy + }, + expectedErr: ErrGenesisRevealPrevOutMismatch, + }, + { + name: "genesis reveal output index mismatch", + assetType: asset.Normal, + amount: &amount, + noMetaHash: true, + genesisRevealMutator: func( + g *asset.Genesis) *asset.Genesis { + + gCopy := *g + gCopy.OutputIndex = uint32( + test.RandInt[int32](), + ) + return &gCopy + }, + expectedErr: ErrGenesisRevealOutputIndexMismatch, + }, + { + name: "group key reveal invalid key", + assetType: asset.Collectible, + noMetaHash: true, + groupRevealMutator: func(gkr *asset.GroupKeyReveal) { + gkr.RawKey[0] = 0x01 + }, + expectedErr: secp256k1.ErrPubKeyInvalidFormat, + }, + { + name: "group key reveal mismatched tweaked key", + assetType: asset.Normal, + amount: &amount, + noMetaHash: true, + groupRevealMutator: func(gkr *asset.GroupKeyReveal) { + gkr.TapscriptRoot = test.RandBytes(32) + }, + expectedErr: ErrGroupKeyRevealMismatch, + }, } testVectors := &TestVectors{} @@ -595,6 +685,7 @@ func TestGenesisProofVerification(t *testing.T) { tt, tc.assetType, tc.amount, tc.tapscriptPreimage, tc.noMetaHash, tc.metaReveal, tc.genesisMutator, + tc.genesisRevealMutator, tc.groupRevealMutator, tc.assetVersion, ) _, err := genesisProof.Verify( @@ -635,7 +726,7 @@ func TestProofBlockHeaderVerification(t *testing.T) { t.Parallel() proof, _ := genRandomGenesisWithProof( - t, asset.Collectible, nil, nil, true, nil, nil, 0, + t, asset.Collectible, nil, nil, true, nil, nil, nil, nil, 0, ) // Create a base reference for the block header and block height. We @@ -793,7 +884,7 @@ func TestProofReplacement(t *testing.T) { amt := uint64(i + 1) assetVersion := asset.Version(i % 2) lotsOfProofs[i], _ = genRandomGenesisWithProof( - t, asset.Normal, &amt, nil, false, nil, nil, + t, asset.Normal, &amt, nil, false, nil, nil, nil, nil, assetVersion, ) } @@ -822,7 +913,7 @@ func TestProofReplacement(t *testing.T) { // We'll generate a random proof, and then replace a random // proof in the file with it. proof, _ := genRandomGenesisWithProof( - t, asset.Normal, &amt, nil, false, nil, nil, + t, asset.Normal, &amt, nil, false, nil, nil, nil, nil, assetVersion, ) idx := test.RandIntn(numProofs) @@ -836,7 +927,7 @@ func TestProofReplacement(t *testing.T) { // boundary conditions). amt := uint64(1337) firstProof, _ := genRandomGenesisWithProof( - t, asset.Normal, &amt, nil, false, nil, nil, asset.V1, + t, asset.Normal, &amt, nil, false, nil, nil, nil, nil, asset.V1, ) err = f.ReplaceProofAt(0, firstProof) require.NoError(t, err) @@ -844,7 +935,7 @@ func TestProofReplacement(t *testing.T) { amt = uint64(2016) lastProof, _ := genRandomGenesisWithProof( - t, asset.Normal, &amt, nil, false, nil, nil, asset.V0, + t, asset.Normal, &amt, nil, false, nil, nil, nil, nil, asset.V0, ) err = f.ReplaceProofAt(uint32(f.NumProofs()-1), lastProof) require.NoError(t, err) @@ -870,7 +961,7 @@ func BenchmarkProofEncoding(b *testing.B) { // Start with a minted genesis asset. genesisProof, _ := genRandomGenesisWithProof( - b, asset.Normal, &amt, nil, false, nil, nil, asset.V0, + b, asset.Normal, &amt, nil, false, nil, nil, nil, nil, asset.V0, ) // We create a file with 10k proofs (the same one) and test encoding/ diff --git a/proof/verifier.go b/proof/verifier.go index 18c3e9b81..28d93ffe2 100644 --- a/proof/verifier.go +++ b/proof/verifier.go @@ -285,20 +285,12 @@ func (p *Proof) verifyGenesisReveal() error { return ErrGenesisRevealRequired } - // The genesis reveal determines the ID of an asset, so make sure it is - // consistent. - assetID := p.Asset.ID() - if reveal.ID() != assetID { - return ErrGenesisRevealAssetIDMismatch - } - - // We also make sure the genesis reveal is consistent with the TLV - // fields in the state transition proof. + // Make sure the genesis reveal is consistent with the TLV fields in + // the state transition proof. if reveal.FirstPrevOut != p.PrevOut { return ErrGenesisRevealPrevOutMismatch } - // TODO(roasbeef): enforce practical limit on size of meta reveal // If this asset has an empty meta reveal, then the meta hash must be // empty. Otherwise, the meta hash must match the meta reveal. var proofMeta [asset.MetaHashLen]byte @@ -318,8 +310,13 @@ func (p *Proof) verifyGenesisReveal() error { return ErrGenesisRevealOutputIndexMismatch } - if reveal.Type != p.Asset.Type { - return ErrGenesisRevealTypeMismatch + // The genesis reveal determines the ID of an asset, so make sure it is + // consistent. Since the asset ID commits to all fields of the genesis, + // this is equivalent to checking equality for the genesis tag and type + // fields that have not yet been verified. + assetID := p.Asset.ID() + if reveal.ID() != assetID { + return ErrGenesisRevealAssetIDMismatch } return nil @@ -341,14 +338,7 @@ func (p *Proof) verifyGenesisGroupKey(groupVerifier GroupVerifier) error { // the same key as the group key specified for the asset. func (p *Proof) verifyGroupKeyReveal() error { groupKey := p.Asset.GroupKey - if groupKey == nil { - return ErrGroupKeyRequired - } - reveal := p.GroupKeyReveal - if reveal == nil { - return ErrGroupKeyRevealRequired - } revealedKey, err := reveal.GroupPubKey(p.Asset.ID()) if err != nil {