diff --git a/asset/asset.go b/asset/asset.go index f54c887f1..81a37fe75 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -18,6 +18,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/mssmt" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/tlv" "golang.org/x/exp/slices" @@ -470,6 +471,20 @@ type GroupKeyReveal struct { TapscriptRoot []byte } +// GroupPubKey returns the group public key derived from the group key reveal. +func (g *GroupKeyReveal) GroupPubKey(assetID ID) (*btcec.PublicKey, error) { + rawKey, err := g.RawKey.ToPubKey() + if err != nil { + return nil, err + } + + internalKey := input.TweakPubKeyWithTweak(rawKey, assetID[:]) + tweakedGroupKey := txscript.ComputeTaprootOutputKey( + internalKey, g.TapscriptRoot[:], + ) + return tweakedGroupKey, nil +} + // IsEqual returns true if this group key and signature are exactly equivalent // to the passed other group key. func (g *GroupKey) IsEqual(otherGroupKey *GroupKey) bool { @@ -759,6 +774,7 @@ func (r *RawKeyGenesisSigner) SignGenesis(keyDesc keychain.KeyDescriptor, return nil, nil, fmt.Errorf("cannot sign with key") } + // TODO(jhb): Update to two-phase tweak tweakedPrivKey := txscript.TweakTaprootPrivKey( *r.privKey, initialGen.GroupKeyTweak(), ) diff --git a/proof/proof.go b/proof/proof.go index 7ee0c3168..25f3ec4b4 100644 --- a/proof/proof.go +++ b/proof/proof.go @@ -53,7 +53,7 @@ var ( // ErrGenesisRevealMetaRevealRequired is an error returned if an asset // proof for a genesis asset has a non-zero meta hash, but doesn't have // a meta reveal. - ErrGenesisRevealMetaRevealRequired = errors.New("genesis reveal meta " + + ErrGenesisRevealMetaRevealRequired = errors.New("genesis meta reveal " + "reveal required") // ErrGenesisRevealMetaHashMismatch is an error returned if an asset @@ -74,6 +74,25 @@ var ( 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 " + + "has group key reveal") + + // ErrGroupKeyRevealMismatch is an error returned if an asset proof for + // a genesis asset has a group key reveal that doesn't match the group + // key. + ErrGroupKeyRevealMismatch = errors.New("group key reveal doesn't " + + "match group key") + + // ErrGroupKeyRevealRequired is an error returned if an asset proof for + // a genesis asset with a group key is missing a group key reveal. + ErrGroupKeyRevealRequired = errors.New("group key reveal required") + + // ErrGroupKeyRequired is an error returned if an asset proof for a + // genesis asset is missing a group key when it should have one. + ErrGroupKeyRequired = errors.New("group key required") + // RegtestTestVectorName is the name of the test vector file that is // generated/updated by an actual integration test run on regtest. It is // exported here, so we can use it in the integration tests. diff --git a/proof/proof_test.go b/proof/proof_test.go index 4f097acf2..1759a2df1 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -354,6 +354,15 @@ func genRandomGenesisWithProof(t testing.TB, assetType asset.Type, } assetGroupKey := asset.RandGroupKey(t, assetGenesis) + groupKeyReveal := asset.GroupKeyReveal{ + RawKey: asset.ToSerialized( + assetGroupKey.RawKey.PubKey, + ), + } + if assetGroupKey.TapscriptRoot != [32]byte{} { + groupKeyReveal.TapscriptRoot = assetGroupKey.TapscriptRoot[:] + } + tapCommitment, assets, err := commitment.Mint( assetGenesis, assetGroupKey, &commitment.AssetDetails{ Type: assetType, @@ -432,6 +441,7 @@ func genRandomGenesisWithProof(t testing.TB, assetType asset.Type, ExclusionProofs: nil, AdditionalInputs: nil, GenesisReveal: &assetGenesis, + GroupKeyReveal: &groupKeyReveal, }, genesisPrivKey } diff --git a/proof/verifier.go b/proof/verifier.go index 9ce1c4e3a..135ee614a 100644 --- a/proof/verifier.go +++ b/proof/verifier.go @@ -320,6 +320,38 @@ func (p *Proof) verifyGenesisReveal() error { return nil } +// verifyGroupKeyReveal verifies that the group key reveal can be used to derive +// 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 + } + + // TODO(jhb): Actually use this key and compare it. + _, err := reveal.GroupPubKey(p.Asset.ID()) + if err != nil { + return err + } + + // TODO(jhb): Enforce this check once we update SignGenesis() to + // implement the same key tweaking as in GroupPubKey(), by passing + // the assetID as a single tweak to SignOutputRaw(). + // Make sure the derived key matches what we expect. + /* + if !groupKey.GroupPubKey.IsEqual(revealedKey) { + return ErrGroupKeyRevealMismatch + } + */ + + return nil +} + // HeaderVerifier is a callback function which returns an error if the given // block header is invalid (usually: not present on chain). type HeaderVerifier func(blockHeader wire.BlockHeader, blockHeight uint32) error @@ -416,7 +448,28 @@ func (p *Proof) Verify(ctx context.Context, prev *AssetSnapshot, } } - // 6. Either a set of asset inputs with valid witnesses is included that + // 6. Verify group key reveal for genesis assets. Not all assets have a + // group key, and should therefore not have a group key reveal. If a + // group key is present, the group key reveal must also be present. + hasGroupKeyReveal := p.GroupKeyReveal != nil + hasGroupKey := p.Asset.GroupKey != nil + switch { + case !isGenesisAsset && hasGroupKeyReveal: + return nil, ErrNonGenesisAssetWithGroupKeyReveal + + case isGenesisAsset && hasGroupKey && !hasGroupKeyReveal: + return nil, ErrGroupKeyRevealRequired + + case isGenesisAsset && !hasGroupKey && hasGroupKeyReveal: + return nil, ErrGroupKeyRequired + + case isGenesisAsset && hasGroupKey && hasGroupKeyReveal: + if err := p.verifyGroupKeyReveal(); err != nil { + return nil, err + } + } + + // 7. Either a set of asset inputs with valid witnesses is included that // satisfy the resulting state transition or a challenge witness is // provided as part of an ownership proof. var splitAsset bool @@ -433,7 +486,7 @@ func (p *Proof) Verify(ctx context.Context, prev *AssetSnapshot, return nil, err } - // 7. At this point we know there is an inclusion proof, which must be + // 8. At this point we know there is an inclusion proof, which must be // a commitment proof. So we can extract the tapscript preimage directly // from there. tapscriptPreimage := p.InclusionProof.CommitmentProof.TapSiblingPreimage