From 0c6f3fff6bbb67eef6f1cefe41868e0a0f1771e2 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 10 Jan 2025 12:48:50 -0800 Subject: [PATCH 1/6] pedersen: add new internal package for pedersen commitments In this commit, we add a new internal package that can be used to create and verify Pedersen commitments. We aim to make the API very simple to use, while providing configuration that's useful for tests and a general set of use cases. We also add property based tests to achieve 100% test coverage of all relevant invariants. --- asset/asset.go | 2 +- internal/pedersen/commitment.go | 147 +++++++ internal/pedersen/commitment_test.go | 181 ++++++++ ...itmentProperties-20250110122316-67631.fail | 199 +++++++++ ...operties_binding-20250110124128-45790.fail | 403 ++++++++++++++++++ ...ties_correctness-20250110123534-19936.fail | 207 +++++++++ ...rties_uniqueness-20250110124128-45790.fail | 357 ++++++++++++++++ tapscript/script.go | 2 +- 8 files changed, 1496 insertions(+), 2 deletions(-) create mode 100644 internal/pedersen/commitment.go create mode 100644 internal/pedersen/commitment_test.go create mode 100644 internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties/TestPedersenCommitmentProperties-20250110122316-67631.fail create mode 100644 internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_binding/TestPedersenCommitmentProperties_binding-20250110124128-45790.fail create mode 100644 internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_correctness/TestPedersenCommitmentProperties_correctness-20250110123534-19936.fail create mode 100644 internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_uniqueness/TestPedersenCommitmentProperties_uniqueness-20250110124128-45790.fail diff --git a/asset/asset.go b/asset/asset.go index bb72632a4..1915f74bb 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -139,7 +139,7 @@ var ( // EmptyGenesisID is the ID of the empty genesis struct. EmptyGenesisID = EmptyGenesis.ID() - // NUMSBytes is the NUMs point we'll use for un-spendable script keys. + // NUMSBytes is the NUMS point we'll use for un-spendable script keys. // It was generated via a try-and-increment approach using the phrase // "taproot-assets" with SHA2-256. The code for the try-and-increment // approach can be seen here: diff --git a/internal/pedersen/commitment.go b/internal/pedersen/commitment.go new file mode 100644 index 000000000..808356fc2 --- /dev/null +++ b/internal/pedersen/commitment.go @@ -0,0 +1,147 @@ +package pedersen + +import ( + "crypto/sha256" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" +) + +var ( + // DefaultNUMS is the default NUMS key used for Pedersen commitments. + DefaultNUMS = input.TaprootNUMSKey + + // one is the value 1 as a scalar value. + one = new(btcec.ModNScalar).SetInt(1) +) + +// Opening is the opening to a Pedersen commitment. It contains a message, and +// an optional mask. If the mask is left off, then the commitment will lose its +// hiding property (two identical messages will map to the same point), but the +// binding property is kept. +type Opening struct { + // Msg is the message that was committed to. + Msg [sha256.Size]byte + + // Mask is the mask used to blind the message. This is typically + // referred to as `r` in the Pedersen commitment literature. + // + // We make this optional, as without it we'll default to no value, which + // means that the commitment loses the hiding attribute, but still + // remains computationally binding. + Mask fn.Option[[sha256.Size]byte] + + // NUMS is an optional value that should be used to verify the + // commitment if a custom NUMS point was used. + NUMS fn.Option[btcec.PublicKey] +} + +// Commitment is a Pedersen commitment of the form: m*G + r*H, where: +// - m is the message being committed together +// - G is the generator point of the curve +// - r is the mask used to blind the messages +// - H is the auxiliary generator point +// +// The commitment is a point on the curve. Given the opening a 3rd party can +// verify the message that was committed to. +type Commitment struct { + // point is the committed point. + point btcec.PublicKey +} + +// commitOpts is a struct that holds the options for creating a commitment. +type commitOpts struct { + // numsPoint is the NUMS point used for the commitment. If this is not + // set, then the default NUMS point will be used. + numsPoint fn.Option[btcec.PublicKey] +} + +// defaultCommitOpts returns the default options for creating a commitment. +func defaultCommitOpts() *commitOpts { + return &commitOpts{} +} + +// commitOpt is a functional option that can be used to modify the default set +// of options. +type commitOpt func(*commitOpts) + +// WithCustomNUMS is a functional option that can be used to set a custom NUMS +// point. +func WithCustomNUMS(h btcec.PublicKey) commitOpt { + return func(o *commitOpts) { + o.numsPoint = fn.Some(h) + } +} + +// commit is a helper function that creates a Pedersen commitment given a +// message, mask, and NUMS point. +func commit(msg [sha256.Size]byte, mask fn.Option[[sha256.Size]byte], + nums btcec.PublicKey) btcec.PublicKey { + + var ( + numsJ btcec.JacobianPoint + blindingPointJ btcec.JacobianPoint + + msgPointJ btcec.JacobianPoint + + commitJ btcec.JacobianPoint + ) + + nums.AsJacobian(&numsJ) + + // First, we'll create the message point, m*G. This maps the message to + // a new EC point. + msgPoint, _ := btcec.PrivKeyFromBytes(msg[:]) + msgPoint.PubKey().AsJacobian(&msgPointJ) + + // With the message point created, we'll now create the blinding point + // using the aux generator H. This will optionally utilize the masking + // value r. From this we derive the blinding point: r*H. + blindingVal := fn.MapOption( + func(r [sha256.Size]byte) *btcec.ModNScalar { + rVal := new(btcec.ModNScalar) + rVal.SetByteSlice(r[:]) + + return rVal + }, + )(mask).UnwrapOr(one) + btcec.ScalarMultNonConst(blindingVal, &numsJ, &blindingPointJ) + + // With the message and blinding point constructed, we'll now add them + // together to obtain our final commitment. + btcec.AddNonConst(&msgPointJ, &blindingPointJ, &commitJ) + + commitJ.ToAffine() + + return *btcec.NewPublicKey(&commitJ.X, &commitJ.Y) +} + +// NewCommitment creates a new Pedersen commitment given an opening. +func NewCommitment(op Opening, opts ...commitOpt) Commitment { + opt := defaultCommitOpts() + for _, o := range opts { + o(opt) + } + + numsPoint := opt.numsPoint.UnwrapOr(DefaultNUMS) + + commitPoint := commit(op.Msg, op.Mask, numsPoint) + + return Commitment{ + point: commitPoint, + } +} + +// Verify verifies that the commitment is valid given the opening. False is +// returned if the commitment doesn't match up. +func (c Commitment) Verify(op Opening) bool { + commitPoint := commit(op.Msg, op.Mask, op.NUMS.UnwrapOr(DefaultNUMS)) + + return c.point.IsEqual(&commitPoint) +} + +// Point returns the underlying point of the commitment. +func (c Commitment) Point() btcec.PublicKey { + return c.point +} diff --git a/internal/pedersen/commitment_test.go b/internal/pedersen/commitment_test.go new file mode 100644 index 000000000..6961d42c1 --- /dev/null +++ b/internal/pedersen/commitment_test.go @@ -0,0 +1,181 @@ +package pedersen + +import ( + "crypto/sha256" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/fn" + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// generateRandomBytes generates a random 256-byte array. +func generateRandomBytes(t *rapid.T) [sha256.Size]byte { + bytes := rapid.SliceOfN( + rapid.Byte(), sha256.Size, sha256.Size, + ).Draw(t, "bytes") + + var result [sha256.Size]byte + copy(result[:], bytes) + + return result +} + +// TestPedersenCommitmentProperties tests various properties of Pedersen +// commitments using property-based testing. +func TestPedersenCommitmentProperties(t *testing.T) { + t.Parallel() + + // Property 1: A commitment should verify with its own opening. + t.Run("correctness", rapid.MakeCheck(func(t *rapid.T) { + msg := generateRandomBytes(t) + mask := generateRandomBytes(t) + + opening := Opening{ + Msg: msg, + Mask: fn.Some(mask), + } + + commitment := NewCommitment(opening) + require.True( + t, commitment.Verify(opening), + "commitment failed to verify with its own opening", + ) + })) + + // Property 2: Different messages should (with very high probability) + // produce different commitments. + t.Run("uniqueness", rapid.MakeCheck(func(t *rapid.T) { + msg1 := generateRandomBytes(t) + msg2 := generateRandomBytes(t) + mask := generateRandomBytes(t) + + // Skip if messages happen to be the same. + if msg1 == msg2 { + return + } + + opening1 := Opening{ + Msg: msg1, + Mask: fn.Some(mask), + } + opening2 := Opening{ + Msg: msg2, + Mask: fn.Some(mask), + } + + commitment1 := NewCommitment(opening1) + commitment2 := NewCommitment(opening2) + + // Different messages should produce different commitments. + require.NotEqual( + t, commitment1, commitment2, + "different messages produced identical commitments", + ) + })) + + // Property 3: Commitments should be binding (cannot find different + // openings that verify for the same commitment). + t.Run("binding", rapid.MakeCheck(func(t *rapid.T) { + msg1 := generateRandomBytes(t) + msg2 := generateRandomBytes(t) + mask1 := generateRandomBytes(t) + mask2 := generateRandomBytes(t) + + // Skip if messages or masks happen to be the same + if msg1 == msg2 || mask1 == mask2 { + return + } + + opening1 := Opening{ + Msg: msg1, + Mask: fn.Some(mask1), + } + opening2 := Opening{ + Msg: msg2, + Mask: fn.Some(mask2), + } + + commitment := NewCommitment(opening1) + + // The commitment should not verify with a different opening + require.False( + t, commitment.Verify(opening2), + "commitment verified with incorrect opening", + ) + })) + + // Property 4: Commitments should work with optional masks (non-hiding + // but still binding). + t.Run("no_mask_none_hiding", rapid.MakeCheck(func(t *rapid.T) { + msg := generateRandomBytes(t) + + opening := Opening{ + Msg: msg, + Mask: fn.None[[sha256.Size]byte](), + } + + commitment := NewCommitment(opening) + require.True( + t, commitment.Verify(opening), + "commitment without mask failed to verify", + ) + + // If we make another opening with the same message, then we + // should get the same commitment, as no mask was used. + opening2 := Opening{ + Msg: msg, + Mask: fn.None[[sha256.Size]byte](), + } + + commitment2 := NewCommitment(opening2) + + require.Equal(t, commitment, commitment2) + })) + + // Property 5: Test with custom NUMS point + t.Run("custom_nums", rapid.MakeCheck(func(t *rapid.T) { + msg := generateRandomBytes(t) + mask := generateRandomBytes(t) + + // Generate a random point for testing + privKey, _ := btcec.NewPrivateKey() + customNUMs := privKey.PubKey() + + opening := Opening{ + Msg: msg, + Mask: fn.Some(mask), + NUMS: fn.Some(*customNUMs), + } + + commitment := NewCommitment( + opening, WithCustomNUMS(*customNUMs), + ) + + require.True( + t, commitment.Verify(opening), + "commitment with custom NUMS failed to verify", + ) + })) + + // Property 6: Same message and mask should always produce the same + // commitment. + t.Run("determinism", rapid.MakeCheck(func(t *rapid.T) { + msg := generateRandomBytes(t) + mask := generateRandomBytes(t) + + opening := Opening{ + Msg: msg, + Mask: fn.Some(mask), + } + + commitment1 := NewCommitment(opening) + commitment2 := NewCommitment(opening) + + require.Equal( + t, commitment1, commitment2, + "same opening produced different commitments", + ) + })) +} diff --git a/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties/TestPedersenCommitmentProperties-20250110122316-67631.fail b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties/TestPedersenCommitmentProperties-20250110122316-67631.fail new file mode 100644 index 000000000..02bf12b0a --- /dev/null +++ b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties/TestPedersenCommitmentProperties-20250110122316-67631.fail @@ -0,0 +1,199 @@ +# 2025/01/10 12:23:16.878522 [TestPedersenCommitmentProperties] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/10 12:23:16.878526 [TestPedersenCommitmentProperties] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2} +# 2025/01/10 12:23:16.878575 [TestPedersenCommitmentProperties] commitment failed to verify with its own opening +# +v0.4.8#360509716493285705 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x38e38e38e38e4 +0x2 +0x0 \ No newline at end of file diff --git a/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_binding/TestPedersenCommitmentProperties_binding-20250110124128-45790.fail b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_binding/TestPedersenCommitmentProperties_binding-20250110124128-45790.fail new file mode 100644 index 000000000..244c234b1 --- /dev/null +++ b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_binding/TestPedersenCommitmentProperties_binding-20250110124128-45790.fail @@ -0,0 +1,403 @@ +# 2025/01/10 12:41:28.271077 [TestPedersenCommitmentProperties/binding] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/10 12:41:28.271081 [TestPedersenCommitmentProperties/binding] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} +# 2025/01/10 12:41:28.271083 [TestPedersenCommitmentProperties/binding] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/10 12:41:28.271085 [TestPedersenCommitmentProperties/binding] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} +# 2025/01/10 12:41:28.271150 [TestPedersenCommitmentProperties/binding] +# Error Trace: /Users/roasbeef/gocode/src/github.com/lightninglabs/taproot-assets/internal/pedersen/commitment_test.go:103 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:368 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:377 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:203 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:138 +# Error: Should be true +# Test: TestPedersenCommitmentProperties/binding +# Messages: commitment verified with incorrect opening +# +v0.4.8#18231256111919915874 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x1 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x1 +0x0 \ No newline at end of file diff --git a/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_correctness/TestPedersenCommitmentProperties_correctness-20250110123534-19936.fail b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_correctness/TestPedersenCommitmentProperties_correctness-20250110123534-19936.fail new file mode 100644 index 000000000..b52ba6d5b --- /dev/null +++ b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_correctness/TestPedersenCommitmentProperties_correctness-20250110123534-19936.fail @@ -0,0 +1,207 @@ +# 2025/01/10 12:35:34.630662 [TestPedersenCommitmentProperties/correctness] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/10 12:35:34.630666 [TestPedersenCommitmentProperties/correctness] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/10 12:35:34.630726 [TestPedersenCommitmentProperties/correctness] +# Error Trace: /Users/roasbeef/gocode/src/github.com/lightninglabs/taproot-assets/internal/pedersen/commitment_test.go:41 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:368 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:377 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:203 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:138 +# Error: Should be true +# Test: TestPedersenCommitmentProperties/correctness +# Messages: commitment failed to verify with its own opening +# +v0.4.8#17976660921723222867 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 \ No newline at end of file diff --git a/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_uniqueness/TestPedersenCommitmentProperties_uniqueness-20250110124128-45790.fail b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_uniqueness/TestPedersenCommitmentProperties_uniqueness-20250110124128-45790.fail new file mode 100644 index 000000000..0b92f928d --- /dev/null +++ b/internal/pedersen/testdata/rapid/TestPedersenCommitmentProperties_uniqueness/TestPedersenCommitmentProperties_uniqueness-20250110124128-45790.fail @@ -0,0 +1,357 @@ +# 2025/01/10 12:41:28.105404 [TestPedersenCommitmentProperties/uniqueness] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/10 12:41:28.105408 [TestPedersenCommitmentProperties/uniqueness] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} +# 2025/01/10 12:41:28.105410 [TestPedersenCommitmentProperties/uniqueness] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/10 12:41:28.105495 [TestPedersenCommitmentProperties/uniqueness] +# Error Trace: /Users/roasbeef/gocode/src/github.com/lightninglabs/taproot-assets/internal/pedersen/commitment_test.go:72 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:368 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:377 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:203 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:138 +# Error: Not equal: +# expected: pedersen.Commitment{point:secp256k1.PublicKey{x:secp256k1.FieldVal{n:[10]uint32{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}, y:secp256k1.FieldVal{n:[10]uint32{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}}} +# actual : pedersen.Commitment{point:secp256k1.PublicKey{x:secp256k1.FieldVal{n:[10]uint32{0x2f81798, 0xa056c5, 0x28d959f, 0x36cb738, 0x3029bfc, 0x3a1c2c1, 0x206295c, 0x2eeb156, 0x27ef9dc, 0x1e6f99}}, y:secp256k1.FieldVal{n:[10]uint32{0x310d4b8, 0x1f423fe, 0x14199c4, 0x1229a15, 0xfd17b4, 0x384422a, 0x24fbfc0, 0x3119576, 0x27726a3, 0x120eb6}}}} +# +# Diff: +# --- Expected +# +++ Actual +# @@ -4,12 +4,12 @@ +# n: ([10]uint32) (len=10) { +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0 +# + (uint32) 49813400, +# + (uint32) 10507973, +# + (uint32) 42833311, +# + (uint32) 57456440, +# + (uint32) 50502652, +# + (uint32) 60932801, +# + (uint32) 33958236, +# + (uint32) 49197398, +# + (uint32) 41875932, +# + (uint32) 1994649 +# } +# @@ -18,12 +18,12 @@ +# n: ([10]uint32) (len=10) { +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0, +# - (uint32) 0 +# + (uint32) 51434680, +# + (uint32) 32777214, +# + (uint32) 21076420, +# + (uint32) 19044885, +# + (uint32) 16586676, +# + (uint32) 58999338, +# + (uint32) 38780864, +# + (uint32) 51484022, +# + (uint32) 41363107, +# + (uint32) 1183414 +# } +# Test: TestPedersenCommitmentProperties/uniqueness +# Messages: different messages produced identical commitments +# +v0.4.8#4540272390961616044 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x1 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 \ No newline at end of file diff --git a/tapscript/script.go b/tapscript/script.go index ce46e17f2..571949146 100644 --- a/tapscript/script.go +++ b/tapscript/script.go @@ -34,7 +34,7 @@ func NewChannelFundingScriptTree() *FundingScriptTree { tapScriptRoot := tapscriptTree.RootNode.TapHash() // Finally, we'll make the funding output script which actually uses a - // NUMs key to force a script path only. + // NUMS key to force a script path only. fundingOutputKey := txscript.ComputeTaprootOutputKey( &input.TaprootNUMSKey, tapScriptRoot[:], ) From 7417d3124768376d2d7092e984392cde4ee56991 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 10 Jan 2025 13:25:34 -0800 Subject: [PATCH 2/6] multi: add new v2 version of GKR based on Pedersen commitments In this commit, we add a new version of the new more flexible GKR type. This version uses an `OP_CHECKSIG` with an un-spendable public key. We derive such a public key by using the Pedersen commitment of the asset ID, or blank message (for the dummy leaf case). As a Pedersen commitment uses an aux generator H, that no one knows the private key to, it's impossible to generate a signature using the commitment point. An informal sketch of a proof by contradiction goes something like: * Starting with a Pedersen commitment, P = m*G + r*H. * Let's assume that we can actually sign with P. This means that there's some x, where P = x*G that we know. * If we re-write x in terms of our commitment, we have: P = m*G + r*H. * Wlog, we'll simplify by removing `r`, the blinding factor, so: P = m*G + H. * In terms of private values we have x*G = m*G + H * If we solve for H, then we have H = (x-m)*G * If this is a Pedersen commitment, then the private key of H must be unknown to all parties. * If we can sign with P, then this means that we know the value (x-m). * However, since this is a Pedersen commitment, it's impossible to know the value `x`, which is a function of the private value H of the pedersen commitment (h*H = (x-m)*G. This is a contradiction, as we see the that if we know x-m, then we also know h. --- asset/asset.go | 147 +++++++++--- asset/group_key_reveal_test.go | 16 +- ...ncodeDecodeRapid-20250113154507-29836.fail | 222 ++++++++++++++++++ proof/proof_test.go | 51 ++-- tapgarden/planter.go | 5 +- tapgarden/planter_test.go | 87 ++++--- 6 files changed, 437 insertions(+), 91 deletions(-) create mode 100644 asset/testdata/rapid/TestGroupKeyRevealEncodeDecodeRapid/TestGroupKeyRevealEncodeDecodeRapid-20250113154507-29836.fail diff --git a/asset/asset.go b/asset/asset.go index 1915f74bb..2886ba98c 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -27,6 +27,7 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/pedersen" "github.com/lightninglabs/taproot-assets/mssmt" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" @@ -957,7 +958,8 @@ func (e *ExternalKey) PubKey() (btcec.PublicKey, error) { // NewGroupKeyV1FromExternal creates a new V1 group key from an external key and // asset ID. The customRootHash is optional and can be used to specify a custom // tapscript root. -func NewGroupKeyV1FromExternal(externalKey ExternalKey, assetID ID, +func NewGroupKeyV1FromExternal(version NonSpendLeafVersion, + externalKey ExternalKey, assetID ID, customRoot fn.Option[chainhash.Hash]) (btcec.PublicKey, chainhash.Hash, error) { @@ -973,7 +975,7 @@ func NewGroupKeyV1FromExternal(externalKey ExternalKey, assetID ID, "(e.g. xpub): %w", err) } - root, err := NewGroupKeyTapscriptRoot(assetID, customRoot) + root, err := NewGroupKeyTapscriptRoot(version, assetID, customRoot) if err != nil { return zeroPubKey, zeroHash, fmt.Errorf("cannot derive group "+ "key reveal tapscript root: %w", err) @@ -1122,21 +1124,48 @@ type GroupKeyReveal interface { // that includes the specified data. If the data is nil, the leaf will not // contain any data but will still be a valid non-spendable script leaf. // -// The script leaf is made non-spendable by including an OP_RETURN opcode at the -// start of the script. While the script can still be executed, it will always -// fail and cannot be used to spend funds. -func NewNonSpendableScriptLeaf(data []byte) (txscript.TapLeaf, error) { - // Construct a script builder and add the OP_RETURN opcode to the start - // of the script to ensure that the script is non-executable. - scriptBuilder := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN) +// The script leaf is made non-spendable by including an OP_RETURN at the start +// of the script (or an OP_CHECKSIG at the end, depending on the version). While +// the script can still be executed, it will always fail and cannot be used to +// spend funds. +func NewNonSpendableScriptLeaf(version NonSpendLeafVersion, + data []byte) (txscript.TapLeaf, error) { + + var builder *txscript.ScriptBuilder + switch version { + // For the OP_RETURN based version, we'll use a single OP_RETURN opcode. + case OpReturnVersion: + builder = txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN) + if data != nil { + builder = builder.AddData(data) + } + + // For the Pedersen commitment based version, we'll use a single + // OP_CEHCKSIG with an un-spendable key. + case PedersenVersion: + var msg [sha256.Size]byte + copy(msg[:], data) + + // Make a Pedersen opening that uses no mask (we don't carry on + // the random value, as we don't care about hiding here). We'll + // also use the existing NUMS point. + op := pedersen.Opening{ + Msg: msg, + } + commitPoint := pedersen.NewCommitment(op).Point() + + commitBytes := schnorr.SerializePubKey(&commitPoint) + + builder = txscript.NewScriptBuilder().AddData(commitBytes). + AddOp(txscript.OP_CHECKSIG) - // Add the data to the script if it is provided. - if data != nil { - scriptBuilder.AddData(data) + default: + return txscript.TapLeaf{}, fmt.Errorf("unknown "+ + "version %v", version) } // Construct script from the script builder. - script, err := scriptBuilder.Script() + script, err := builder.Script() if err != nil { return txscript.TapLeaf{}, fmt.Errorf("failed to construct "+ "non-spendable script: %w", err) @@ -1209,12 +1238,12 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { // hash may represent either a single leaf or the root hash of an entire // subtree. // -// If `custom_root_hash` is not provided, it defaults to a bare `[OP_RETURN]` as +// If `custom_root_hash` is not provided, it defaults to a bare `non_spend()` as // well. In this case, no valid script spending path can correspond to the // custom subtree root hash due to the pre-image resistance of SHA-256. // // A sibling node is included alongside the `custom_root_hash` node. This -// sibling is a non-spendable script leaf containing `[OP_RETURN]`. Its +// sibling is a non-spendable script leaf containing `non_spend()`. Its // presence ensures that one of the two positions in the first layer of the // tapscript tree is occupied by a branch node. Due to the pre-image // resistance of SHA-256, this prevents the existence of a second recognizable @@ -1224,24 +1253,36 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { // // [tapscript_root] // / \ -// [OP_RETURN ] [tweaked_custom_branch] +// [non_spend()] [tweaked_custom_branch] // / \ -// [OP_RETURN] +// [non_spend()] // // Where: // - [tapscript_root] is the root of the final tapscript tree. -// - [OP_RETURN ] is a first-layer non-spendable script +// - [non_spend()] is a first-layer non-spendable script // leaf that commits to the genesis asset ID. // - [tweaked_custom_branch] is a branch node that serves two purposes: // 1. It cannot be misinterpreted as a genesis asset ID leaf. // 2. It optionally includes user-defined script spending leaves. // - is the root hash of the custom tapscript subtree. -// If not specified, it defaults to the same [OP_RETURN] that's on the left +// If not specified, it defaults to the same non_spend() that's on the left // side. -// - [OP_RETURN] is a non-spendable script leaf containing the script -// `OP_RETURN`. Its presence ensures that [tweaked_custom_branch] remains -// a branch node and cannot be a valid genesis asset ID leaf. +// - non_spend(data) is a non-spendable script leaf that contains the data +// argument. The data can be nil/empty in which case, the un-spendable +// script doesn't commit to the data. Its presence ensures that +// [tweaked_custom_branch] remains a branch node and cannot be a valid +// genesis asset ID leaf. Two non-spendable script leaves are possible: +// - One that uses an OP_RETURN to create a script that will "return +// early" and terminate the script execution. +// - One that uses a normal OP_CHECKSIG operator where the pubkey +// argument is a key that cannot be signed with. We generate this +// special public key using a Pedersen commitment, where the message is +// the asset ID (or 32 all-zero bytes in case data is nil/empty). type GroupKeyRevealTapscript struct { + // version is the version of the group key reveal that determines how + // the non-spendable leaf is created. + version NonSpendLeafVersion + // root is the final tapscript root after all tapscript tweaks have // been applied. The asset group key is derived from this root and the // internal key. @@ -1270,7 +1311,7 @@ type GroupKeyRevealTapscript struct { // subtree root hash. // // nolint: lll -func NewGroupKeyTapscriptRoot(genesisAssetID ID, +func NewGroupKeyTapscriptRoot(version NonSpendLeafVersion, genesisAssetID ID, customRoot fn.Option[chainhash.Hash]) (GroupKeyRevealTapscript, error) { // First, we compute the tweaked custom branch hash. This hash is @@ -1279,7 +1320,7 @@ func NewGroupKeyTapscriptRoot(genesisAssetID ID, // // If a custom tapscript subtree root hash is provided, we use it. // Otherwise, we default to an empty non-spendable leaf hash as well. - emptyNonSpendLeaf, err := NewNonSpendableScriptLeaf(nil) + emptyNonSpendLeaf, err := NewNonSpendableScriptLeaf(version, nil) if err != nil { return GroupKeyRevealTapscript{}, err } @@ -1294,7 +1335,7 @@ func NewGroupKeyTapscriptRoot(genesisAssetID ID, // asset ID leaf hash to compute the final tapscript root hash. // // Construct a non-spendable tapscript leaf for the genesis asset ID. - assetIDLeaf, err := NewNonSpendableScriptLeaf(genesisAssetID[:]) + assetIDLeaf, err := NewNonSpendableScriptLeaf(version, genesisAssetID[:]) if err != nil { return GroupKeyRevealTapscript{}, err } @@ -1318,6 +1359,7 @@ func NewGroupKeyTapscriptRoot(genesisAssetID ID, ) return GroupKeyRevealTapscript{ + version: version, root: rootHash, customSubtreeRoot: customRoot, customSubtreeInclusionProof: customSubtreeInclusionProof, @@ -1329,7 +1371,9 @@ func NewGroupKeyTapscriptRoot(genesisAssetID ID, func (g *GroupKeyRevealTapscript) Validate(assetID ID) error { // Compute the final tapscript root hash from the genesis asset ID and // the custom tapscript subtree root hash. - tapscript, err := NewGroupKeyTapscriptRoot(assetID, g.customSubtreeRoot) + tapscript, err := NewGroupKeyTapscriptRoot( + g.version, assetID, g.customSubtreeRoot, + ) if err != nil { return fmt.Errorf("failed to compute tapscript root hash: %w", err) @@ -1356,12 +1400,30 @@ func (g *GroupKeyRevealTapscript) Root() chainhash.Hash { return g.root } +// NonSpendLeafVersion is the version of the group key reveal. +// +// Version 1 is the original version that's based on an OP_RETURN. +// +// Version 2 is a follow-up version that instead uses a Pedersen commitment. +type NonSpendLeafVersion = uint8 + +const ( + // OpReturnVersion is the version of the group key reveal that uses an + // OP_RETURN. + OpReturnVersion NonSpendLeafVersion = 1 + + // PedersenVersion is the version of the group key reveal that uses a + // Pedersen commitment. + PedersenVersion NonSpendLeafVersion = 2 +) + // GroupKeyRevealV1 is a version 1 group key reveal type for representing the // data used to derive and verify the tweaked key used to identify an asset // group. type GroupKeyRevealV1 struct { - // version is the version of the group key reveal. - version uint8 + // version is the version of the group key reveal that determines how + // the non-spendable leaf is created. + version NonSpendLeafVersion // internalKey refers to the internal key used to derive the asset // group key. Typically, this internal key is the user's signing public @@ -1384,8 +1446,9 @@ func NewGroupKeyReveal(groupKey GroupKey, genesisAssetID ID) (GroupKeyReveal, switch groupKey.Version { case GroupKeyV1: gkr, err := NewGroupKeyRevealV1( - *groupKey.RawKey.PubKey, genesisAssetID, - groupKey.CustomTapscriptRoot, + // TODO(guggero): Make this configurable in the future. + PedersenVersion, *groupKey.RawKey.PubKey, + genesisAssetID, groupKey.CustomTapscriptRoot, ) if err != nil { return nil, err @@ -1405,13 +1468,13 @@ func NewGroupKeyReveal(groupKey GroupKey, genesisAssetID ID) (GroupKeyReveal, } // NewGroupKeyRevealV1 creates a new version 1 group key reveal instance. -func NewGroupKeyRevealV1(internalKey btcec.PublicKey, - genesisAssetID ID, +func NewGroupKeyRevealV1(version NonSpendLeafVersion, + internalKey btcec.PublicKey, genesisAssetID ID, customRoot fn.Option[chainhash.Hash]) (GroupKeyRevealV1, error) { // Compute the final tapscript root. gkrTapscript, err := NewGroupKeyTapscriptRoot( - genesisAssetID, customRoot, + version, genesisAssetID, customRoot, ) if err != nil { return GroupKeyRevealV1{}, fmt.Errorf("failed to generate "+ @@ -1419,7 +1482,7 @@ func NewGroupKeyRevealV1(internalKey btcec.PublicKey, } return GroupKeyRevealV1{ - version: 1, + version: version, internalKey: ToSerialized(&internalKey), tapscript: gkrTapscript, }, nil @@ -1450,7 +1513,8 @@ func (g *GroupKeyRevealV1) ScriptSpendControlBlock( // root. if len(g.tapscript.customSubtreeInclusionProof) == 0 { gkrTapscript, err := NewGroupKeyTapscriptRoot( - genesisAssetID, g.tapscript.customSubtreeRoot, + g.version, genesisAssetID, + g.tapscript.customSubtreeRoot, ) if err != nil { return txscript.ControlBlock{}, fmt.Errorf("failed to "+ @@ -1532,9 +1596,18 @@ func (g *GroupKeyRevealV1) Decode(r io.Reader, buf *[8]byte, l uint64) error { fn.Some[chainhash.Hash](customSubtreeRoot) } + // Thread the version through to the reveal tapscript, which is needed + // for the validation context. + g.tapscript.version = g.version + return nil } +// Version returns the commitment version of the group key reveal V1. +func (g *GroupKeyRevealV1) Version() NonSpendLeafVersion { + return g.version +} + // RawKey returns the raw key of the group key reveal. func (g *GroupKeyRevealV1) RawKey() SerializedKey { return g.internalKey @@ -2172,7 +2245,9 @@ func (req *GroupKeyRequest) NewGroupPubKey(genesisAssetID ID) (btcec.PublicKey, } groupPubKey, _, err := NewGroupKeyV1FromExternal( - externalKey, genesisAssetID, req.CustomTapscriptRoot, + // TODO(guggero): Make version configurable. + PedersenVersion, externalKey, genesisAssetID, + req.CustomTapscriptRoot, ) if err != nil { return btcec.PublicKey{}, fmt.Errorf("cannot derive group "+ diff --git a/asset/group_key_reveal_test.go b/asset/group_key_reveal_test.go index 4d09b2760..257cb7ba7 100644 --- a/asset/group_key_reveal_test.go +++ b/asset/group_key_reveal_test.go @@ -16,6 +16,8 @@ import ( type testCaseGkrEncodeDecode struct { testName string + version NonSpendLeafVersion + internalKey btcec.PublicKey genesisAssetID ID customSubtreeRoot fn.Option[chainhash.Hash] @@ -24,7 +26,8 @@ type testCaseGkrEncodeDecode struct { // GroupKeyReveal generates a GroupKeyReveal instance from the test case. func (tc testCaseGkrEncodeDecode) GroupKeyReveal() (GroupKeyReveal, error) { gkr, err := NewGroupKeyRevealV1( - tc.internalKey, tc.genesisAssetID, tc.customSubtreeRoot, + tc.version, tc.internalKey, tc.genesisAssetID, + tc.customSubtreeRoot, ) return &gkr, err @@ -52,6 +55,7 @@ func TestGroupKeyRevealEncodeDecode(t *testing.T) { { testName: "no custom root", + version: OpReturnVersion, internalKey: internalKey, genesisAssetID: genesisAssetID, customSubtreeRoot: fn.None[chainhash.Hash](), @@ -59,6 +63,7 @@ func TestGroupKeyRevealEncodeDecode(t *testing.T) { { testName: "with custom root", + version: PedersenVersion, internalKey: internalKey, genesisAssetID: genesisAssetID, customSubtreeRoot: customSubtreeRoot, @@ -167,6 +172,14 @@ func TestGroupKeyRevealEncodeDecodeRapid(tt *testing.T) { // Randomly decide whether to include a custom script. hasCustomScript := rapid.Bool().Draw(t, "has_custom_script") + // Version should be either 1 or 2. + var version NonSpendLeafVersion + if rapid.Bool().Draw(t, "version") { + version = OpReturnVersion + } else { + version = PedersenVersion + } + // If a custom script is included, generate a random script leaf // and subtree root. var customSubtreeRoot fn.Option[chainhash.Hash] @@ -190,6 +203,7 @@ func TestGroupKeyRevealEncodeDecodeRapid(tt *testing.T) { // Create a new GroupKeyReveal instance from the random test // inputs. gkrV1, err := NewGroupKeyRevealV1( + version, internalKey, genesisAssetID, customSubtreeRoot, diff --git a/asset/testdata/rapid/TestGroupKeyRevealEncodeDecodeRapid/TestGroupKeyRevealEncodeDecodeRapid-20250113154507-29836.fail b/asset/testdata/rapid/TestGroupKeyRevealEncodeDecodeRapid/TestGroupKeyRevealEncodeDecodeRapid-20250113154507-29836.fail new file mode 100644 index 000000000..36454f7b8 --- /dev/null +++ b/asset/testdata/rapid/TestGroupKeyRevealEncodeDecodeRapid/TestGroupKeyRevealEncodeDecodeRapid-20250113154507-29836.fail @@ -0,0 +1,222 @@ +# 2025/01/13 15:45:07.315685 [TestGroupKeyRevealEncodeDecodeRapid] [rapid] draw internal_key_bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/13 15:45:07.315698 [TestGroupKeyRevealEncodeDecodeRapid] [rapid] draw genesis_id: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +# 2025/01/13 15:45:07.315699 [TestGroupKeyRevealEncodeDecodeRapid] [rapid] draw has_custom_script: false +# 2025/01/13 15:45:07.315699 [TestGroupKeyRevealEncodeDecodeRapid] [rapid] draw version: false +# 2025/01/13 15:45:07.315803 [TestGroupKeyRevealEncodeDecodeRapid] +# Error Trace: /Users/roasbeef/gocode/src/github.com/lightninglabs/taproot-assets/asset/group_key_reveal_test.go:234 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:368 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:377 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:203 +# /Users/roasbeef/gocode/pkg/mod/pgregory.net/rapid@v1.1.0/engine.go:118 +# /Users/roasbeef/gocode/src/github.com/lightninglabs/taproot-assets/asset/group_key_reveal_test.go:159 +# Error: Not equal: +# expected: &asset.GroupKeyRevealV1{version:0x2, internalKey:asset.SerializedKey{0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, tapscript:asset.GroupKeyRevealTapscript{version:0x2, root:chainhash.Hash{0x9b, 0xd1, 0x3e, 0x20, 0x89, 0x9b, 0x79, 0x47, 0x51, 0x41, 0x5a, 0x79, 0x69, 0xa8, 0xd5, 0xf, 0x45, 0x9c, 0x8d, 0x65, 0xd2, 0xa6, 0x3d, 0x75, 0xda, 0x19, 0xce, 0xaf, 0xaf, 0x6f, 0x88, 0x47}, customSubtreeRoot:fn.Option[github.com/btcsuite/btcd/chaincfg/chainhash.Hash]{isSome:false, some:chainhash.Hash{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}, customSubtreeInclusionProof:[]uint8(nil)}} +# actual : &asset.GroupKeyRevealV1{version:0x2, internalKey:asset.SerializedKey{0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, tapscript:asset.GroupKeyRevealTapscript{version:0x0, root:chainhash.Hash{0x9b, 0xd1, 0x3e, 0x20, 0x89, 0x9b, 0x79, 0x47, 0x51, 0x41, 0x5a, 0x79, 0x69, 0xa8, 0xd5, 0xf, 0x45, 0x9c, 0x8d, 0x65, 0xd2, 0xa6, 0x3d, 0x75, 0xda, 0x19, 0xce, 0xaf, 0xaf, 0x6f, 0x88, 0x47}, customSubtreeRoot:fn.Option[github.com/btcsuite/btcd/chaincfg/chainhash.Hash]{isSome:false, some:chainhash.Hash{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}, customSubtreeInclusionProof:[]uint8(nil)}} +# +# Diff: +# --- Expected +# +++ Actual +# @@ -8,3 +8,3 @@ +# tapscript: (asset.GroupKeyRevealTapscript) { +# - version: (uint8) 2, +# + version: (uint8) 0, +# root: (chainhash.Hash) (len=32) { +# Test: TestGroupKeyRevealEncodeDecodeRapid +# +v0.4.8#15521290734392816414 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 \ No newline at end of file diff --git a/proof/proof_test.go b/proof/proof_test.go index ef4060769..0926c588c 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -126,6 +126,7 @@ func assertEqualGroupKeyRevealV1(t *testing.T, expected, actual asset.GroupKeyRevealV1, expectedGenesisAssetID asset.ID) { require.Equal(t, expected.RawKey(), actual.RawKey()) + require.Equal(t, expected.Version(), actual.Version()) // Compare the tapscript root. Normalize nil to empty slice for // comparison. @@ -209,11 +210,14 @@ func assertEqualProof(t *testing.T, expected, actual *Proof) { require.Equal(t, expected.TxMerkleProof, actual.TxMerkleProof) require.Equal(t, expected.Asset, actual.Asset) - assertEqualTaprootProof(t, &expected.InclusionProof, &actual.InclusionProof) + assertEqualTaprootProof( + t, &expected.InclusionProof, &actual.InclusionProof, + ) for i := range expected.ExclusionProofs { assertEqualTaprootProof( - t, &expected.ExclusionProofs[i], &actual.ExclusionProofs[i], + t, &expected.ExclusionProofs[i], + &actual.ExclusionProofs[i], ) } @@ -239,7 +243,9 @@ func assertEqualProof(t *testing.T, expected, actual *Proof) { len(expected.AdditionalInputs), ) for j := range expected.AdditionalInputs[i].proofs { - e, err := expected.AdditionalInputs[i].ProofAt(uint32(j)) + e, err := expected.AdditionalInputs[i].ProofAt( + uint32(j), + ) require.NoError(t, err) a, err := actual.AdditionalInputs[i].ProofAt(uint32(j)) @@ -268,28 +274,37 @@ func TestProofEncodingGroupKeyRevealV1(t *testing.T) { scriptKey := test.RandPubKey(t) proof := RandProof(t, genesis, scriptKey, oddTxBlock, 0, 1) - // Override the group key reveal with a V1 reveal. internalKey := test.RandPubKey(t) customRoot := chainhash.Hash(test.RandBytes(32)) - groupKeyReveal, err := asset.NewGroupKeyRevealV1( - *internalKey, genesis.ID(), fn.Some(customRoot), - ) - require.NoError(t, err) - proof.GroupKeyReveal = &groupKeyReveal + versions := []asset.NonSpendLeafVersion{ + asset.OpReturnVersion, asset.PedersenVersion, + } + for _, version := range versions { + // Override the group key reveal with a V1 reveal. + groupKeyReveal, err := asset.NewGroupKeyRevealV1( + version, *internalKey, genesis.ID(), + fn.Some(customRoot), + ) + require.NoError(t, err) - file, err := NewFile(V0, proof, proof) - require.NoError(t, err) - proof.AdditionalInputs = []File{*file, *file} + proof.GroupKeyReveal = &groupKeyReveal - var proofBuf bytes.Buffer - require.NoError(t, proof.Encode(&proofBuf)) - proofBytes := proofBuf.Bytes() + file, err := NewFile(V0, proof, proof) + require.NoError(t, err) + proof.AdditionalInputs = []File{*file, *file} - var decodedProof Proof - require.NoError(t, decodedProof.Decode(bytes.NewReader(proofBytes))) + var proofBuf bytes.Buffer + require.NoError(t, proof.Encode(&proofBuf)) + proofBytes := proofBuf.Bytes() - assertEqualProof(t, &proof, &decodedProof) + var decodedProof Proof + require.NoError( + t, decodedProof.Decode(bytes.NewReader(proofBytes)), + ) + + assertEqualProof(t, &proof, &decodedProof) + } } func TestProofEncoding(t *testing.T) { diff --git a/tapgarden/planter.go b/tapgarden/planter.go index a3534642c..0319cea0c 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -847,10 +847,13 @@ func buildGroupReqs(genesisPoint wire.OutPoint, assetOutputIndex uint32, // through grafting. // // At this point, we are constructing the group - // tapscript tree root whether or not the + // tapscript tree root whether the // customRootHash is defined. tapscriptTree, err := asset.NewGroupKeyTapscriptRoot( + // TODO(guggero): Make this + // configurable in the future. + asset.PedersenVersion, assetGen.ID(), customRootHash, ) if err != nil { diff --git a/tapgarden/planter_test.go b/tapgarden/planter_test.go index 353f07d8e..4b3f35ce6 100644 --- a/tapgarden/planter_test.go +++ b/tapgarden/planter_test.go @@ -2096,11 +2096,13 @@ func TestGroupKeyRevealV1WitnessWithCustomRoot(t *testing.T) { hashLockLeaf, schnorrSigLeaf, ).RootNode.TapHash() - spendTestCases := []struct { + type testCase struct { name string genWitness func(*testing.T, *asset.Asset, asset.GroupKeyRevealV1) wire.TxWitness - }{{ + } + + spendTestCases := []testCase{{ name: "key spend", genWitness: func(t *testing.T, a *asset.Asset, gkr asset.GroupKeyRevealV1) wire.TxWitness { @@ -2177,41 +2179,55 @@ func TestGroupKeyRevealV1WitnessWithCustomRoot(t *testing.T) { }, }} - for _, tc := range spendTestCases { - t.Run(tc.name, func(tt *testing.T) { - randAsset := asset.RandAsset(tt, asset.Normal) - genAssetID := randAsset.ID() - groupKeyReveal, err := asset.NewGroupKeyRevealV1( - *internalKeyDesc.PubKey, genAssetID, - fn.Some(userRoot), - ) - require.NoError(tt, err) + runTestCase := func(tt *testing.T, tc testCase, + version asset.NonSpendLeafVersion) { - // Set the group key on the asset, since it's a randomly - // created group key otherwise. - groupPubKey, err := groupKeyReveal.GroupPubKey( - genAssetID, - ) - require.NoError(tt, err) - randAsset.GroupKey = &asset.GroupKey{ - RawKey: internalKeyDesc, - GroupPubKey: *groupPubKey, - TapscriptRoot: groupKeyReveal.TapscriptRoot(), - } - randAsset.PrevWitnesses = []asset.Witness{ - { - PrevID: &asset.PrevID{}, - }, - } + randAsset := asset.RandAsset(tt, asset.Normal) + genAssetID := randAsset.ID() + groupKeyReveal, err := asset.NewGroupKeyRevealV1( + version, *internalKeyDesc.PubKey, genAssetID, + fn.Some(userRoot), + ) + require.NoError(tt, err) + + // Set the group key on the asset, since it's a randomly created + // group key otherwise. + groupPubKey, err := groupKeyReveal.GroupPubKey(genAssetID) + require.NoError(tt, err) + + gkr := groupKeyReveal + randAsset.GroupKey = &asset.GroupKey{ + RawKey: internalKeyDesc, + GroupPubKey: *groupPubKey, + TapscriptRoot: gkr.TapscriptRoot(), + } + randAsset.PrevWitnesses = []asset.Witness{ + { + PrevID: &asset.PrevID{}, + }, + } - witness := tc.genWitness(tt, randAsset, groupKeyReveal) - randAsset.PrevWitnesses[0].TxWitness = witness + witness := tc.genWitness(tt, randAsset, groupKeyReveal) + randAsset.PrevWitnesses[0].TxWitness = witness - err = txValidator.Execute( - randAsset, nil, nil, proof.MockChainLookup, - ) - require.NoError(tt, err) - }) + err = txValidator.Execute( + randAsset, nil, nil, proof.MockChainLookup, + ) + require.NoError(tt, err) + } + + gkrVersions := []asset.NonSpendLeafVersion{ + asset.OpReturnVersion, + asset.PedersenVersion, + } + + for _, tc := range spendTestCases { + for _, version := range gkrVersions { + name := fmt.Sprintf("%s:%v", tc.name, version) + t.Run(name, func(tt *testing.T) { + runTestCase(tt, tc, version) + }) + } } } @@ -2238,7 +2254,8 @@ func TestGroupKeyRevealV1WitnessNoScripts(t *testing.T) { randAsset := asset.RandAsset(t, asset.Normal) genAssetID := randAsset.ID() groupKeyReveal, err := asset.NewGroupKeyRevealV1( - *internalKeyDesc.PubKey, genAssetID, fn.None[chainhash.Hash](), + asset.OpReturnVersion, *internalKeyDesc.PubKey, genAssetID, + fn.None[chainhash.Hash](), ) require.NoError(t, err) From 101516f613631ae72c0cc5988972a509a9167e62 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 13 Jan 2025 16:44:09 -0800 Subject: [PATCH 3/6] asset: add test case for NewNonSpendableScriptLeaf This asserts that both variants of the leaf we generate are actually nonspendable. --- asset/group_key_reveal_test.go | 123 +++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/asset/group_key_reveal_test.go b/asset/group_key_reveal_test.go index 257cb7ba7..0046f67da 100644 --- a/asset/group_key_reveal_test.go +++ b/asset/group_key_reveal_test.go @@ -2,11 +2,13 @@ package asset import ( "bytes" + "math/rand" "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/internal/test" "github.com/stretchr/testify/require" @@ -162,6 +164,12 @@ func TestGroupKeyRevealEncodeDecodeRapid(tt *testing.T) { // Generate a random internal key. internalKeyBytes := rapid.SliceOfN(rapid.Byte(), 32, 32). Draw(t, "internal_key_bytes") + + // The internal key bytes shouldn't be all zero. + if bytes.Equal(internalKeyBytes, make([]byte, 32)) { + return + } + _, publicKey := btcec.PrivKeyFromBytes(internalKeyBytes) internalKey := *publicKey @@ -273,3 +281,118 @@ func TestGroupKeyRevealEncodeDecodeRapid(tt *testing.T) { } }) } + +// TestNonSpendableLeafScript tests that the unspendable leaf script is actually +// unspendable. +func TestNonSpendableLeafScript(t *testing.T) { + var assetID ID + _, err := rand.Read(assetID[:]) + require.NoError(t, err) + + internalKey := test.RandPubKey(t) + + const amt = 1000 + + testCases := []struct { + name string + + version NonSpendLeafVersion + errString string + }{ + + { + name: "op_return", + version: OpReturnVersion, + errString: "script returned early", + }, + { + name: "pedersen", + version: PedersenVersion, + errString: "signature not empty on failed checksig", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // For this test, we'll just have the test leaf be the + // only element in the script tree. + testLeaf, err := NewNonSpendableScriptLeaf( + testCase.version, assetID[:], + ) + require.NoError(t, err) + + // From the script tree, we'll then create the taproot + // output public key. + scriptTree := txscript.AssembleTaprootScriptTree( + testLeaf, + ) + rootHash := scriptTree.RootNode.TapHash() + outputKey := txscript.ComputeTaprootOutputKey( + internalKey, rootHash[:], + ) + + // Finally, we'll make the dummy spend transaction, and + // the output script that we'll attempt to spend. + spendTx := wire.NewMsgTx(1) + spendTx.AddTxIn(&wire.TxIn{}) + + leafScript, err := txscript.PayToTaprootScript( + outputKey, + ) + require.NoError(t, err) + + prevOuts := txscript.NewCannedPrevOutputFetcher( + leafScript, amt, + ) + sigHash := txscript.NewTxSigHashes(spendTx, prevOuts) + + // If this is the Pedersen variant, then we'll actually + // need to generate a signature. + var sig []byte + if testCase.version == PedersenVersion { + privKey, _ := btcec.PrivKeyFromBytes(assetID[:]) + + sig, err = txscript.RawTxInTapscriptSignature( + spendTx, sigHash, 0, amt, leafScript, + testLeaf, txscript.SigHashAll, privKey, + ) + require.NoError(t, err) + } + + proofs := scriptTree.LeafMerkleProofs[0] + ctrlBlock := proofs.ToControlBlock(internalKey) + ctrlBockBytes, err := ctrlBlock.ToBytes() + require.NoError(t, err) + + // The final witness template is just the script, then + // the control block. + finalWitness := wire.TxWitness{ + testLeaf.Script, ctrlBockBytes, + } + + // If we have a sig, then we'll add this on as well to + // ensure that even a well crafted signature is + // rejected. + if sig != nil { + finalWitness = append( + [][]byte{sig}, finalWitness..., + ) + } + + spendTx.TxIn[0].Witness = finalWitness + + // Finally, we'll execute the spend. This should fail if + // the leaf is actually unspendable. + vm, err := txscript.NewEngine( + leafScript, spendTx, 0, + txscript.StandardVerifyFlags, nil, sigHash, + amt, prevOuts, + ) + require.NoError(t, err) + + err = vm.Execute() + require.Error(t, err) + require.ErrorContains(t, err, testCase.errString) + }) + } +} From e83fdc85e4d50490df6b99ed18e0127af42b76a2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 15 Jan 2025 14:20:05 +0100 Subject: [PATCH 4/6] asset: make compatible with policy based signing To achieve hardware wallet signing support, we remove any siblings from the tree if there are no custom scripts present. This allows us to use the Pedersen commitment to create a public key miniscript policy which is compatible with certain hardware wallets. --- asset/asset.go | 72 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 2886ba98c..2fd1c72ed 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -1223,11 +1223,12 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { // The tapscript tree is formulated to guarantee that only one recognizable // genesis asset ID can exist in the tree. The ID is uniquely placed in the // first leaf layer, which contains exactly two nodes: the ID leaf and its -// sibling. The sibling node is deliberately constructed to ensure it cannot be -// mistaken for a genesis asset ID leaf. +// sibling (if present). The sibling node is deliberately constructed to ensure +// it cannot be mistaken for a genesis asset ID leaf. // // The sibling node, `[tweaked_custom_branch]`, of the genesis asset ID leaf is -// a branch node by design. It serves two purposes: +// a branch node by design and is only required if the user wants to use custom +// scripts. It serves two purposes: // 1. It ensures that only one genesis asset ID leaf can exist in the first // layer, as it is not a valid genesis asset ID leaf. // 2. It optionally supports user-defined script spending leaves, enabling @@ -1238,10 +1239,6 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { // hash may represent either a single leaf or the root hash of an entire // subtree. // -// If `custom_root_hash` is not provided, it defaults to a bare `non_spend()` as -// well. In this case, no valid script spending path can correspond to the -// custom subtree root hash due to the pre-image resistance of SHA-256. -// // A sibling node is included alongside the `custom_root_hash` node. This // sibling is a non-spendable script leaf containing `non_spend()`. Its // presence ensures that one of the two positions in the first layer of the @@ -1265,8 +1262,35 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { // 1. It cannot be misinterpreted as a genesis asset ID leaf. // 2. It optionally includes user-defined script spending leaves. // - is the root hash of the custom tapscript subtree. -// If not specified, it defaults to the same non_spend() that's on the left -// side. +// If not specified, the whole right branch [tweaked_custom_branch] is +// omitted (see below). +// - non_spend(data) is a non-spendable script leaf that contains the data +// argument. The data can be nil/empty in which case, the un-spendable +// script doesn't commit to the data. Its presence ensures that +// [tweaked_custom_branch] remains a branch node and cannot be a valid +// genesis asset ID leaf. Two non-spendable script leaves are possible: +// - One that uses an OP_RETURN to create a script that will "return +// early" and terminate the script execution. +// - One that uses a normal OP_CHECKSIG operator where the pubkey +// argument is a key that cannot be signed with. We generate this +// special public key using a Pedersen commitment, where the message is +// the asset ID (or 32 all-zero bytes in case data is nil/empty). +// +// If `custom_root_hash` is not provided, then there is no sibling to the asset +// ID leaf, meaning the tree only has a single leaf. This makes it possible to +// turn the single asset ID leaf into a miniscript policy, either using +// raw(hex(OP_RETURN )) or pk(). +// The final tapscript tree with no custom scripts adopts the following +// structure: +// +// [tapscript_root] +// | +// [non_spend()] +// +// Where: +// - [tapscript_root] is the root of the final tapscript tree. +// - [non_spend()] is a first-layer non-spendable script +// leaf that commits to the genesis asset ID. // - non_spend(data) is a non-spendable script leaf that contains the data // argument. The data can be nil/empty in which case, the un-spendable // script doesn't commit to the data. Its presence ensures that @@ -1325,26 +1349,32 @@ func NewGroupKeyTapscriptRoot(version NonSpendLeafVersion, genesisAssetID ID, return GroupKeyRevealTapscript{}, err } - // Compute the tweaked custom branch hash. - tweakedCustomBranchHash := TapBranchHash( - emptyNonSpendLeaf.TapHash(), - customRoot.UnwrapOr(emptyNonSpendLeaf.TapHash()), - ) - // Next, we'll combine the tweaked custom branch hash with the genesis // asset ID leaf hash to compute the final tapscript root hash. // // Construct a non-spendable tapscript leaf for the genesis asset ID. - assetIDLeaf, err := NewNonSpendableScriptLeaf(version, genesisAssetID[:]) + assetIDLeaf, err := NewNonSpendableScriptLeaf( + version, genesisAssetID[:], + ) if err != nil { return GroupKeyRevealTapscript{}, err } - // Compute final tapscript root hash. This is the root hash of the - // tapscript tree that is used to derive the asset group key. - rootHash := TapBranchHash( - assetIDLeaf.TapHash(), tweakedCustomBranchHash, - ) + // Compute the tweaked custom branch hash or leaf, depending on whether + // we have a custom tapscript subtree root hash. We move the + // un-spendable leaf to level 1 if there is no custom root hash. This is + // mainly due to the fact that we require valid scripts in order to have + // hardware wallet support. An empty leaf cannot be represented as a + // list of scripts in a PSBT. That also means that custom scripts are + // currently not compatible with miniscript policy based hardware + // wallets. + rootHash := assetIDLeaf.TapHash() + customRoot.WhenSome(func(customRoot chainhash.Hash) { + rightHash := TapBranchHash( + emptyNonSpendLeaf.TapHash(), customRoot, + ) + rootHash = TapBranchHash(assetIDLeaf.TapHash(), rightHash) + }) // Construct the custom subtree inclusion proof. This proof is required // to spend custom tapscript leaves in the tapscript tree. From 38573e74df815c86d7871350fc339312b71b7e0a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 15 Jan 2025 14:31:26 +0100 Subject: [PATCH 5/6] asset: don't store non-serialized inclusion proof This commit removes a field from the group key reveal V1 that wasn't serialized and calculated on demand if necessary anyway. Re-computing it when it is needed is a smaller cost than the added complexity of having a non-serialized field. --- asset/asset.go | 88 ++++++++++++---------------------- asset/group_key_reveal_test.go | 5 -- tapgarden/planter.go | 2 +- 3 files changed, 32 insertions(+), 63 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 2fd1c72ed..98943c9b0 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -975,7 +975,7 @@ func NewGroupKeyV1FromExternal(version NonSpendLeafVersion, "(e.g. xpub): %w", err) } - root, err := NewGroupKeyTapscriptRoot(version, assetID, customRoot) + root, _, err := NewGroupKeyTapscriptRoot(version, assetID, customRoot) if err != nil { return zeroPubKey, zeroHash, fmt.Errorf("cannot derive group "+ "key reveal tapscript root: %w", err) @@ -1317,26 +1317,15 @@ type GroupKeyRevealTapscript struct { // tapscript tree. This subtree may define script spending conditions // associated with the group key. customSubtreeRoot fn.Option[chainhash.Hash] - - // customSubtreeInclusionProof provides the inclusion proof for the - // custom tapscript subtree. It is required to spend the custom - // tapscript leaves within the tree. - // - // NOTE: This field should not be serialized as part of the group key - // reveal. It is included here to ensure it is constructed concurrently - // with the tapscript root, maintaining consistency and minimizing - // errors. - customSubtreeInclusionProof []byte } // NewGroupKeyTapscriptRoot computes the final tapscript root hash // which is used to derive the asset group key. The final tapscript root // hash is computed from the genesis asset ID and an optional custom tapscript // subtree root hash. -// -// nolint: lll func NewGroupKeyTapscriptRoot(version NonSpendLeafVersion, genesisAssetID ID, - customRoot fn.Option[chainhash.Hash]) (GroupKeyRevealTapscript, error) { + customRoot fn.Option[chainhash.Hash]) (GroupKeyRevealTapscript, []byte, + error) { // First, we compute the tweaked custom branch hash. This hash is // derived by combining the hash of a non-spendable leaf and the root @@ -1346,7 +1335,7 @@ func NewGroupKeyTapscriptRoot(version NonSpendLeafVersion, genesisAssetID ID, // Otherwise, we default to an empty non-spendable leaf hash as well. emptyNonSpendLeaf, err := NewNonSpendableScriptLeaf(version, nil) if err != nil { - return GroupKeyRevealTapscript{}, err + return GroupKeyRevealTapscript{}, nil, err } // Next, we'll combine the tweaked custom branch hash with the genesis @@ -1357,7 +1346,7 @@ func NewGroupKeyTapscriptRoot(version NonSpendLeafVersion, genesisAssetID ID, version, genesisAssetID[:], ) if err != nil { - return GroupKeyRevealTapscript{}, err + return GroupKeyRevealTapscript{}, nil, err } // Compute the tweaked custom branch hash or leaf, depending on whether @@ -1381,19 +1370,16 @@ func NewGroupKeyTapscriptRoot(version NonSpendLeafVersion, genesisAssetID ID, emptyNonSpendLeafHash := emptyNonSpendLeaf.TapHash() assetIDLeafHash := assetIDLeaf.TapHash() - customSubtreeInclusionProof := bytes.Join( - [][]byte{ - emptyNonSpendLeafHash[:], - assetIDLeafHash[:], - }, nil, - ) + customSubtreeInclusionProof := bytes.Join([][]byte{ + emptyNonSpendLeafHash[:], + assetIDLeafHash[:], + }, nil) return GroupKeyRevealTapscript{ - version: version, - root: rootHash, - customSubtreeRoot: customRoot, - customSubtreeInclusionProof: customSubtreeInclusionProof, - }, nil + version: version, + root: rootHash, + customSubtreeRoot: customRoot, + }, customSubtreeInclusionProof, nil } // Validate checks that the group key reveal tapscript is well-formed and @@ -1401,7 +1387,7 @@ func NewGroupKeyTapscriptRoot(version NonSpendLeafVersion, genesisAssetID ID, func (g *GroupKeyRevealTapscript) Validate(assetID ID) error { // Compute the final tapscript root hash from the genesis asset ID and // the custom tapscript subtree root hash. - tapscript, err := NewGroupKeyTapscriptRoot( + tapscript, _, err := NewGroupKeyTapscriptRoot( g.version, assetID, g.customSubtreeRoot, ) if err != nil { @@ -1503,7 +1489,7 @@ func NewGroupKeyRevealV1(version NonSpendLeafVersion, customRoot fn.Option[chainhash.Hash]) (GroupKeyRevealV1, error) { // Compute the final tapscript root. - gkrTapscript, err := NewGroupKeyTapscriptRoot( + gkrTapscript, _, err := NewGroupKeyTapscriptRoot( version, genesisAssetID, customRoot, ) if err != nil { @@ -1520,8 +1506,6 @@ func NewGroupKeyRevealV1(version NonSpendLeafVersion, // ScriptSpendControlBlock returns the control block for the script spending // path in the custom tapscript subtree. -// -// nolint: lll func (g *GroupKeyRevealV1) ScriptSpendControlBlock( genesisAssetID ID) (txscript.ControlBlock, error) { @@ -1537,39 +1521,29 @@ func (g *GroupKeyRevealV1) ScriptSpendControlBlock( outputKeyIsOdd := outputKey.SerializeCompressed()[0] == secp256k1.PubKeyFormatCompressedOdd - // If the custom subtree inclusion proof is nil, it may not have been - // set during decoding. Compute it, set it on the group key reveal, and - // validate both the computed tapscript root against the expected - // root. - if len(g.tapscript.customSubtreeInclusionProof) == 0 { - gkrTapscript, err := NewGroupKeyTapscriptRoot( - g.version, genesisAssetID, - g.tapscript.customSubtreeRoot, - ) - if err != nil { - return txscript.ControlBlock{}, fmt.Errorf("failed to "+ - "generate tapscript artifacts: %w", err) - } - - // Ensure that the computed tapscript root matches the expected - // root. - if !gkrTapscript.root.IsEqual(&g.tapscript.root) { - return txscript.ControlBlock{}, fmt.Errorf("tapscript "+ - "root mismatch (expected=%s, computed=%s)", - g.tapscript.root, gkrTapscript.root) - } + // We now re-calculate the group key reveal tapscript root, which also + // gives us the inclusion proof for the custom tapscript subtree. + gkrTapscript, inclusionProof, err := NewGroupKeyTapscriptRoot( + g.version, genesisAssetID, g.tapscript.customSubtreeRoot, + ) + if err != nil { + return txscript.ControlBlock{}, fmt.Errorf("failed to "+ + "generate tapscript artifacts: %w", err) + } - // Set the custom subtree inclusion proof on the group key - // reveal. - g.tapscript.customSubtreeInclusionProof = - gkrTapscript.customSubtreeInclusionProof + // Ensure that the computed tapscript root matches the expected + // root. + if !gkrTapscript.root.IsEqual(&g.tapscript.root) { + return txscript.ControlBlock{}, fmt.Errorf("tapscript "+ + "root mismatch (expected=%s, computed=%s)", + g.tapscript.root, gkrTapscript.root) } return txscript.ControlBlock{ InternalKey: internalKey, OutputKeyYIsOdd: outputKeyIsOdd, LeafVersion: txscript.BaseLeafVersion, - InclusionProof: g.tapscript.customSubtreeInclusionProof, + InclusionProof: inclusionProof, }, nil } diff --git a/asset/group_key_reveal_test.go b/asset/group_key_reveal_test.go index 0046f67da..b287a13c6 100644 --- a/asset/group_key_reveal_test.go +++ b/asset/group_key_reveal_test.go @@ -102,7 +102,6 @@ func TestGroupKeyRevealEncodeDecode(t *testing.T) { // encoding/decoding. gkrV1, ok := gkr.(*GroupKeyRevealV1) require.True(tt, ok) - gkrV1.tapscript.customSubtreeInclusionProof = nil // Compare decoded group key reveal with the original. require.Equal(tt, gkrV1, gkrDecoded) @@ -234,10 +233,6 @@ func TestGroupKeyRevealEncodeDecodeRapid(tt *testing.T) { ) require.NoError(t, err) - // Prepare for comparison by removing non-encoded fields from - // the original GroupKeyReveal. - gkrV1.tapscript.customSubtreeInclusionProof = nil - // Compare decoded with original. require.Equal(t, &gkrV1, gkrDecoded) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 0319cea0c..675a27ffd 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -849,7 +849,7 @@ func buildGroupReqs(genesisPoint wire.OutPoint, assetOutputIndex uint32, // At this point, we are constructing the group // tapscript tree root whether the // customRootHash is defined. - tapscriptTree, err := + tapscriptTree, _, err := asset.NewGroupKeyTapscriptRoot( // TODO(guggero): Make this // configurable in the future. From 9733b3d476bd9fb4ed66f8cb83d2ca98bad9c790 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 15 Jan 2025 14:41:05 +0100 Subject: [PATCH 6/6] asset: turn Pederson key into xpub for hardware wallet support Hardware wallets that support miniscript policies only accept a pk() fragment that specifies an extended key, for example with: pk(@1/<0;1>/*). When signing the transaction the actual derivation path to use (for example 0/0) is defined in the PSBT itself. So the signing policy is generic and the user only has to accept it once. A child key derived from an extended NUMS key is still a NUMS key, as it's just tweaked again. The initial private key is still unknown. --- asset/asset.go | 88 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 98943c9b0..23c75bdc3 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -20,6 +20,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -1143,19 +1144,21 @@ func NewNonSpendableScriptLeaf(version NonSpendLeafVersion, // For the Pedersen commitment based version, we'll use a single // OP_CEHCKSIG with an un-spendable key. case PedersenVersion: + // Make sure we don't accidentally truncate the data. + if len(data) > sha256.Size { + return txscript.TapLeaf{}, fmt.Errorf("data too large") + } + var msg [sha256.Size]byte copy(msg[:], data) - // Make a Pedersen opening that uses no mask (we don't carry on - // the random value, as we don't care about hiding here). We'll - // also use the existing NUMS point. - op := pedersen.Opening{ - Msg: msg, + _, commitPoint, err := TweakedNumsKey(msg) + if err != nil { + return txscript.TapLeaf{}, fmt.Errorf("failed to "+ + "derive tweaked NUMS key: %w", err) } - commitPoint := pedersen.NewCommitment(op).Point() - - commitBytes := schnorr.SerializePubKey(&commitPoint) + commitBytes := schnorr.SerializePubKey(commitPoint) builder = txscript.NewScriptBuilder().AddData(commitBytes). AddOp(txscript.OP_CHECKSIG) @@ -1203,6 +1206,65 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { return tlv.MakePrimitiveRecord(GKRCustomSubtreeRoot, (*[32]byte)(root)) } +// NumsXPub turns the given NUMS key into an extended public key (using the x +// coordinate of the public key as the chain code), then derives the actual key +// to use from the derivation path 0/0. The extended key always has the mainnet +// version, but can be converted to any network on demand by the caller with +// CloneWithVersion(). +func NumsXPub(numsKey btcec.PublicKey) (*hdkeychain.ExtendedKey, + *btcec.PublicKey, error) { + + keyBytes := numsKey.SerializeCompressed() + chainCode := keyBytes[1:] + + // We use a depth of 3, emulating BIP44/49/84/86 style derivation for + // xpubs. We also always use mainnet to not require the caller to pass + // in the net params. Converting to another network is possible with + // CloneWithVersion(). + const depth = 3 + extendedNumsKey := hdkeychain.NewExtendedKey( + chaincfg.MainNetParams.HDPublicKeyID[:], keyBytes, chainCode, + []byte{0, 0, 0, 0}, depth, 0, false, + ) + + // Derive the actual key to use from the xpub. + changeBranch, err := extendedNumsKey.Derive(0) + if err != nil { + return nil, nil, err + } + + indexBranch, err := changeBranch.Derive(0) + if err != nil { + return nil, nil, err + } + + actualKey, err := indexBranch.ECPubKey() + if err != nil { + return nil, nil, err + } + + return extendedNumsKey, actualKey, nil +} + +// TweakedNumsKey derives the NUMS key from the given data, then creates the +// extended key from it and derives the actual (derived child) key to use from +// the derivation path 0/0. The extended key always has the mainnet version, but +// can be converted to any network on demand by the caller with +// CloneWithVersion(). +func TweakedNumsKey(msg [32]byte) (*hdkeychain.ExtendedKey, *btcec.PublicKey, + error) { + + // Make a Pedersen opening that uses no mask (we don't carry on + // the random value, as we don't care about hiding here). We'll + // also use the existing NUMs point. + op := pedersen.Opening{ + Msg: msg, + } + commitPoint := pedersen.NewCommitment(op).Point() + + return NumsXPub(commitPoint) +} + // GroupKeyRevealTapscript holds data used to derive the tapscript root, which // is then used to calculate the asset group key. // @@ -1274,7 +1336,10 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { // - One that uses a normal OP_CHECKSIG operator where the pubkey // argument is a key that cannot be signed with. We generate this // special public key using a Pedersen commitment, where the message is -// the asset ID (or 32 all-zero bytes in case data is nil/empty). +// the asset ID (or 32 all-zero bytes in case data is nil/empty). To achieve +// hardware wallet support, that key is then turned into an extended key +// (xpub) and a child key at path 0/0 is used as the actual public key that +// goes into the OP_CHECKSIG script. // // If `custom_root_hash` is not provided, then there is no sibling to the asset // ID leaf, meaning the tree only has a single leaf. This makes it possible to @@ -1301,7 +1366,10 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { // - One that uses a normal OP_CHECKSIG operator where the pubkey // argument is a key that cannot be signed with. We generate this // special public key using a Pedersen commitment, where the message is -// the asset ID (or 32 all-zero bytes in case data is nil/empty). +// the asset ID (or 32 all-zero bytes in case data is nil/empty). To +// achieve hardware wallet support, that key is then turned into an extended +// key (xpub) and a child key at path 0/0 is used as the actual public key +// that goes into the OP_CHECKSIG script. type GroupKeyRevealTapscript struct { // version is the version of the group key reveal that determines how // the non-spendable leaf is created.