Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GroupKeyReveal V1 #1246

Merged
merged 12 commits into from
Dec 19, 2024
Merged

Add GroupKeyReveal V1 #1246

merged 12 commits into from
Dec 19, 2024

Conversation

ffranr
Copy link
Contributor

@ffranr ffranr commented Dec 9, 2024

This PR introduces a new group reveal structure in which the tapscript tree is tweaked instead of the internal key being modified with the asset ID. As a result, an external PSBT signer no longer needs to handle the tweaking of an internal key prior to signing.

Work towards solving: #1226


This change is Reviewable

@ffranr ffranr requested review from guggero and GeorgeTsagk December 9, 2024 16:57
@ffranr ffranr self-assigned this Dec 9, 2024
@dstadulis dstadulis added this to the v0.6 milestone Dec 9, 2024
@ffranr
Copy link
Contributor Author

ffranr commented Dec 9, 2024

I've added a long doc comment to GroupKeyRevealTapscript. Please review what I've described there. Let me know if you agree/disagree.

@coveralls
Copy link

coveralls commented Dec 9, 2024

Pull Request Test Coverage Report for Build 12405194991

Details

  • 236 of 310 (76.13%) changed or added relevant lines in 4 files are covered.
  • 19 unchanged lines in 6 files lost coverage.
  • Overall coverage increased (+0.1%) to 40.831%

Changes Missing Coverage Covered Lines Changed/Added Lines %
proof/records.go 10 11 90.91%
asset/encoding.go 25 36 69.44%
asset/asset.go 200 262 76.34%
Files with Coverage Reduction New Missed Lines %
tapgarden/planter.go 2 74.12%
commitment/tap.go 2 83.69%
asset/asset.go 3 80.76%
universe/interface.go 4 51.95%
tapdb/universe.go 4 80.91%
tapgarden/caretaker.go 4 68.5%
Totals Coverage Status
Change from base Build 12379489363: 0.1%
Covered Lines: 26043
Relevant Lines: 63783

💛 - Coveralls

Copy link
Member

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only looked at the Godoc comment of GroupKeyRevealTapscript.

asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
proof/encoding.go Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
proof/encoding.go Outdated Show resolved Hide resolved
@ffranr ffranr force-pushed the add-new-groupkeyreveal branch 2 times, most recently from edb5d85 to f1d33e0 Compare December 10, 2024 19:07
@ffranr ffranr requested review from guggero and Roasbeef and removed request for GeorgeTsagk December 10, 2024 19:30
@ffranr ffranr force-pushed the add-new-groupkeyreveal branch 8 times, most recently from 984d24b to 90687e1 Compare December 12, 2024 17:05
Copy link
Member

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I think if we can just get this part into v0.5.0 we should be set for future upgrades.

asset/asset.go Outdated Show resolved Hide resolved
proof/encoding.go Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated
finalRoot chainhash.Hash

// customTapscriptRoot is the optional root of a custom tapscript tree
// that includes script spend conditions for the group key.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We should mention here that when this is None, from the point of view of the user, the group key doesn't have any scripts (so basically BIP-086). But because we always insert two layers of siblings, it'll never actually be a BIP-086 key (the tapscript root will never be empty).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customTapscriptRoot is the user provided subtree hash. And finalRoot is the tapscript tree root hash which incorporates the asset ID and the user provided subtree.

finalRoot is never none going forwards because the tree will at least include asset ID.

customTapscriptRoot can be none because there may not be any user provided subtree.

asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated

// RawKey returns the raw key of the group key reveal.
func (g *GroupKeyRevealV1) RawKey() SerializedKey {
return g.tapInternalKey
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to hide these fields behind methods? Any downside to just exporting them directly?

Copy link
Contributor Author

@ffranr ffranr Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they are used in tests mostly for GroupKeyRevealV0.

asset/asset.go Show resolved Hide resolved
taprpc/group_key_tapscript_test.go Outdated Show resolved Hide resolved
@ffranr ffranr force-pushed the add-new-groupkeyreveal branch 2 times, most recently from 31fb2d1 to 26f8891 Compare December 13, 2024 14:32
Removed to make the code clearer.
@ffranr ffranr force-pushed the add-new-groupkeyreveal branch 3 times, most recently from bfb8a2a to 7cfeac8 Compare December 17, 2024 19:15
@ffranr ffranr force-pushed the add-new-groupkeyreveal branch 2 times, most recently from 6d27d92 to c04fbbb Compare December 17, 2024 20:30
@ffranr ffranr marked this pull request as ready for review December 17, 2024 20:30
@ffranr
Copy link
Contributor Author

ffranr commented Dec 17, 2024

The one thing we need to deal with (probably) is:

func GroupKeyRevealRecord(reveal *asset.GroupKeyReveal) tlv.Record {
	recordSize := func() uint64 {
		// TODO(ffranr): Do we need dispatch on record size? I think
		//  this is used for encoding only. And perhaps only to
		//  determine whether there's anything to encode at all?
		if reveal == nil || *reveal == nil {
			return 0
		}
		r := *reveal
		return uint64(
			btcec.PubKeyBytesLenCompressed + len(r.TapscriptRoot()),
		)
	}
	return tlv.MakeDynamicRecord(
		GroupKeyRevealType, reveal, recordSize,
		asset.GroupKeyRevealEncoder, asset.GroupKeyRevealDecoder,
	)
}

I didn't resolve this TODO.

@ffranr
Copy link
Contributor Author

ffranr commented Dec 17, 2024

The one thing we need to deal with (probably) is:

func GroupKeyRevealRecord(reveal *asset.GroupKeyReveal) tlv.Record {
	recordSize := func() uint64 {
		// TODO(ffranr): Do we need dispatch on record size? I think
		//  this is used for encoding only. And perhaps only to
		//  determine whether there's anything to encode at all?
		if reveal == nil || *reveal == nil {
			return 0
		}
		r := *reveal
		return uint64(
			btcec.PubKeyBytesLenCompressed + len(r.TapscriptRoot()),
		)
	}
	return tlv.MakeDynamicRecord(
		GroupKeyRevealType, reveal, recordSize,
		asset.GroupKeyRevealEncoder, asset.GroupKeyRevealDecoder,
	)
}

I didn't resolve this TODO.

Yes, not sure what to do here.

I think recordSize is only relevant for encoding. The problem is that GroupKeyRevealV1 is encoded as a TLV stream, so I’m not sure how to calculate the size of the encoded byte array. I don’t think it’s just the sum of the field sizes because of the TLV metadata.

@ffranr ffranr requested a review from guggero December 17, 2024 20:53
Copy link
Member

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work on this, thanks for the extra push to completion!!!

Approach/Concept ACK. Code looks very good too. But will want to do another pass tomorrow with a fresh mind (but don't expect any significant changes).

asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
proof/records.go Outdated Show resolved Hide resolved
@Roasbeef Roasbeef added the P0 label Dec 18, 2024
@Roasbeef
Copy link
Member

I think recordSize is only relevant for encoding. The problem is that GroupKeyRevealV1 is encoded as a TLV stream, so I’m not sure how to calculate the size of the encoded byte array

So that's just used to know what the outer length prefix should be. You can either compute a static value as you did there, or just encode it (without the type and length prefix) to see what the size would be if written as a normal TLV stream.

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work!

Did an initial pass through the diff, at times suggesting changes with in-line code comments. I think the only thing missing here is some tests at the proof.Proof level, to make sure things can be properly encoded/decoded with the new reveal type. We should be able to tack this onto the existing randomized tests and test vector generation.

asset/asset.go Outdated
return tlv.ErrRecordTooLarge
}

if l < btcec.PubKeyBytesLenCompressed {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stye nit: could be combined above into a switch statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit is a code move/copy. I was trying not to make any code changes other than copying to maximise clarity and minimise the review burden. I agree that switch statement would be nice! Perhaps after this I can do some refactoring.

proof/encoding.go Outdated Show resolved Hide resolved
asset/asset.go Outdated Show resolved Hide resolved
asset/asset.go Outdated
)

func NewGKRVersionRecord(
version *uint8) tlv.Record {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, for just about all these fields (primitive fields) you can use the new RecordT type, here's an example: https://github.com/lightningnetwork/lnd/blob/8ecef03315f749ae8820b4735978ca2e0552de2e/lnwire/musig2.go#L22-L24

So you can just have a single struct, and declare all the fields as attributes, it would look something like:

type (
   GKRVersionType           = new(tlv.TlvType0)
   GKRInternalKeyType       = new(tlv.TlvType2)
   GKRTapsciptRootType     = new(tlv.TlvType4)
   GKRCustomSubtreeRootType = new(tlv.TlvType7)
)
type GRoupKeyRevealV1 struct {
    Version tlv.RecordT[GKRVersionType, uint32]

    InternalKey tlv.RecordT[GKRInternalKeyType, *btcec.PublicKey]

   TapscriptRoot tlv.RecordT[GKRTapsciptRootType, [sha256.Size]byte]
  
   CustomTreeRoot tlv.Record[GKRCustomSubtreeRootType, [sha256.Size]byte]
}

Then your encode+decode is something like:

func (g *GRoupKeyRevealV1) Encode(w io.Writer) error {
    	stream, err := tlv.NewStream(
		g.Version.Record(), g.InternalKey.Record(), g.TapscriptRoot.Record(),g.CustomTreeRoot.Record(),
	)
	if err != nil {
		return err
	}

	return stream.Encode(w)
}

func (g *GRoupKeyRevealV1) Decode(r io.Reader) error {
  	stream, err := tlv.NewStream(
		g.Version.Record(), g.InternalKey.Record(), g.TapscriptRoot.Record(),g.CustomTreeRoot.Record(),
	)
	if err != nil {
		return err
	}

	return stream.Decode(r)
}

func eGkrV1(w io.Writer, val interface{}, _ *[8]byte) error {
	if v, ok := val.(*GRoupKeyRevealV1); ok {
		return v.Encode(w)
	}

	return tlv.NewTypeForEncodingErr(val, "*GRoupKeyRevealV1")
}

func dGkrV1(r io.Reader, val interface{}, _ *[8]byte, l uint64) error {
	if v, ok := val.(*GRoupKeyRevealV1); ok {
		return v.Decode(r)
	}

	return tlv.NewTypeForDecodingErr(val, "*GRoupKeyRevealV1", l, l)
}

func (g *GRoupKeyRevealV1) Record() tlv.Record {
	sizeFunc := func() uint64 {
		var buf bytes.Buffer
		err := pgEncode(&buf)
		if err != nil {
			panic(err)
		}
		return uint64(len(buf.Bytes()))
	}

	return tlv.MakeDynamicRecord(
		0, p, sizeFunc, eGkrV1, dGkrv1,
	)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I think we can even add the Record() method to the old interface. So you just call that when makign the stream to handle encode/decode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I played around with using tlv.RecordT fields for a while but I couldn't get everything to work correctly.

I got lost in the weeds a bit trying to change all the types and modifying how the fields were used. I got thrown converting customSubtreeRoot fn.Option[chainhash.Hash] into a tlv.OptionalRecordT. Maybe the key was to drop chainhash.Hash in favour of [sha256.Size]byte.

I wouldn't mind giving the tlv.RecordT approach another go, I've managed to use it in RFQ requestWireMsgData, but would you mind if I do that after rc3?

// reveal. It is included here to ensure it is constructed concurrently
// with the tapscript root, maintaining consistency and minimizing
// errors.
customSubtreeInclusionProof []byte
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this isn't just txscript.ControlBlock?

Why wouldn't it be serialized as part of the reveal? It includes the internal key and also the inclusion proof all the way up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking here is not to include txscript.ControlBlock in the serialization to avoid redundancy. Breakdown:

  • InternalKey: Already serialized separately.
  • OutputKeyYIsOdd: Derivable from internal key and tapscript root.
  • LeafVersion: Typically constant; serialization might not be necessary?
  • InclusionProof: Derivable from the genesis asset ID which should be available separately whenever group key reveal is at hand.

I'm aiming to eliminate data duplication, ensuring that each field serves as a definitive "source of truth." And also reduce serialization size.

I suppose if the default value of LeafVersion changes we could version bump group key reveal? Maybe we should just include it now?

asset/asset.go Outdated
//
// nolint: lll
func (g *GroupKeyRevealV1) ScriptSpendControlBlock(
genesisAssetID ID) lfn.Result[txscript.ControlBlock] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe wrap this in another type of a PartialControl block? This gives the proof up to the custom tapscript root, but a spender will still need to provide the other merkle authentication hashes.

Copy link
Contributor Author

@ffranr ffranr Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm good point.

I would like to revise this for 0.6. Right now, it's useful for testing (checking the tapscript root) so perhaps it's sufficient as is for rc3?

func (g *GroupKeyRevealV1) Decode(r io.Reader, buf *[8]byte, l uint64) error {
var customSubtreeRoot chainhash.Hash

tlvStream, err := tlv.NewStream(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, I see now the decomposition of the reveal struct, and the auxiliary tapscript proof information.

@@ -478,19 +480,40 @@ func GroupKeyRevealEncoder(w io.Writer, val any, _ *[8]byte) error {
}

func GroupKeyRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
// Return early if the val is not a pointer to a GroupKeyReveal.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is now actually a pointer to an interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If val is asserted to be of type GroupKeyReveal rather than *GroupKeyReveal, and val == nil, the type assertion inside GroupKeyRevealDecoder (e.g., typ, ok := val.(GroupKeyReveal)) will fail. This happens because Go's type assertion mechanism requires both a dynamic type and a value to confirm the type, and a nil value with no dynamic type results in ok == false.

For example:

var gkrDecoded GroupKeyReveal
var scratchBuffDecode [8]byte
err := GroupKeyRevealDecoder(
    &buffer, gkrDecoded, &scratchBuffDecode,
    uint64(buffer.Len()),
)

This will fail because GroupKeyRevealDecoder cannot determine whether val implements the GroupKeyReveal interface when val == nil. Since gkrDecoded is not a pointer, it has no dynamic type in this context, making the assertion invalid.

By contrast, if val is of type *GroupKeyReveal, the dynamic type is always present, even when the value is nil. This allows GroupKeyRevealDecoder to qualify val == nil correctly.

I think there might be a way around this using the reflect package but I haven't gotten it to work yet.

proof/records.go Outdated Show resolved Hide resolved
return &gkr, err
}

func TestGroupKeyTapscriptEncodeDecode(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a quick mafs version (may need some tweaking) of this test that uses rapid:

func TestGroupKeyTapscriptEncodeDecode(tt *testing.T) {
	ttt.Parallel()

	rapid.Check(tt, func(t *rapid.T) {
		// Generate random test inputs using rapid generators
		internalKey := *rapid.Custom(func(t *rapid.T) *btcec.PublicKey {
			priv, err := btcec.NewPrivateKey()
			if err != nil {
				t.Fatal(err)
			}
			return priv.PubKey()
		}).Draw(t, "internal_key")

		genesisAssetID := ID(rapid.SliceOfN([]byte{}, 32, 32).Draw(t, "genesis_id"))

		// Randomly decide whether to include a custom script
		hasCustomScript := rapid.Bool().Draw(t, "has_custom_script")

		var customSubtreeRoot fn.Option[chainhash.Hash]
		var customScriptLeaf *txscript.TapLeaf

		if hasCustomScript {
			// Generate random script between 1-100 bytes
			scriptSize := rapid.IntRange(1, 100).Draw(t, "script_size")
			customScript := rapid.SliceOfN([]byte{}, scriptSize, scriptSize).Draw(t, "custom_script")
			
			customScriptLeaf = txscript.NewBaseTapLeaf(customScript)
			customSubtreeRoot = fn.Some(customScriptLeaf.TapHash())
		} else {
			customSubtreeRoot = fn.None[chainhash.Hash]()
		}

		// Create and test GroupKeyReveal
		gkr, err := NewGroupKeyRevealV1(
			internalKey, 
			genesisAssetID,
			customSubtreeRoot,
		).Unpack()
		require.NoError(t, err)

		groupPubKey, err := gkr.GroupPubKey(genesisAssetID)
		require.NoError(t, err)

		// Test encoding/decoding
		var buffer bytes.Buffer
		var scratchBuffEncode [8]byte
		err = GroupKeyRevealEncoder(&buffer, &gkr, &scratchBuffEncode)
		require.NoError(t, err)

		var gkrDecoded GroupKeyReveal
		var scratchBuffDecode [8]byte
		err = GroupKeyRevealDecoder(
			&buffer,
			&gkrDecoded,
			&scratchBuffDecode,
			uint64(buffer.Len()),
		)
		require.NoError(t, err)

		// Prepare for comparison by removing non-encoded fields
		gkrV1, ok := gkr.(*GroupKeyRevealV1)
		require.True(t, ok)
		gkrV1.tapscript.customSubtreeInclusionProof = nil

		// Compare decoded with original
		require.Equal(t, gkrV1, gkrDecoded)

		// Verify decoded group public key
		groupPubKeyDecoded, err := gkrDecoded.GroupPubKey(genesisAssetID)
		require.NoError(t, err)
		require.Equal(t, groupPubKey, groupPubKeyDecoded)

		// Test custom subtree if present
		if customSubtreeRoot.IsSome() && customScriptLeaf != nil {
			gkrDecodedV1, ok := gkrDecoded.(*GroupKeyRevealV1)
			require.True(t, ok)

			ctrlBlock, err := gkrDecodedV1.ScriptSpendControlBlock(
				genesisAssetID,
			).Unpack()
			require.NoError(t, err)

			computedRoot := chainhash.Hash(
				ctrlBlock.RootHash(customScriptLeaf.Script),
			)
			require.Equal(t, gkrDecodedV1.tapscript.root, computedRoot)
		}
	})
}

Could be made more succinct with custom generators.

Copy link
Contributor Author

@ffranr ffranr Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest commits will include this test 👍 Thanks!

@guggero
Copy link
Member

guggero commented Dec 18, 2024

Here's my validation test code that makes sure we can create valid witnesses for group keys with script paths (feel free to add without any attribution):

// TestGroupKeyRevealV1WitnessWithCustomRoot tests the different possible spend
// paths for a group key reveal witness if there are custom scripts.
func TestGroupKeyRevealV1WitnessWithCustomRoot(t *testing.T) {
	var (
		ctx              = context.Background()
		mockKeyRing      = tapgarden.NewMockKeyRing()
		mockSigner       = tapgarden.NewMockGenSigner(mockKeyRing)
		txBuilder        = &tapscript.GroupTxBuilder{}
		txValidator      = &tap.ValidatorV0{}
		hashLockPreimage = []byte("foobar")
	)

	// We expect two keys to be derived from the mock.
	go func() {
		<-mockKeyRing.ReqKeys
		<-mockKeyRing.ReqKeys
	}()

	// The internal key is for the actual internal key of the group.
	internalKeyDesc, err := mockKeyRing.DeriveNextTaprootAssetKey(ctx)
	require.NoError(t, err)

	// The second key is used for a signature spend within a tapscript leaf
	// of the custom tapscript tree.
	secondKeyDesc, err := mockKeyRing.DeriveNextTaprootAssetKey(ctx)
	require.NoError(t, err)

	hashLockLeaf := test.ScriptHashLock(t, hashLockPreimage)
	schnorrSigLeaf := test.ScriptSchnorrSig(t, secondKeyDesc.PubKey)

	userRoot := txscript.AssembleTaprootScriptTree(
		hashLockLeaf, schnorrSigLeaf,
	).RootNode.TapHash()

	spendTestCases := []struct {
		name       string
		genWitness func(*testing.T, *asset.Asset,
			asset.GroupKeyRevealV1) wire.TxWitness
	}{{
		name: "key spend",
		genWitness: func(t *testing.T, a *asset.Asset,
			gkr asset.GroupKeyRevealV1) wire.TxWitness {

			genTx, prevOut, err := txBuilder.BuildGenesisTx(a)
			require.NoError(t, err)

			witness, err := signGroupKeyV1(
				internalKeyDesc, gkr, genTx, prevOut,
				mockSigner, nil,
			)
			require.NoError(t, err)

			return witness
		},
	}, {
		name: "script spend with preimage",
		genWitness: func(t *testing.T, a *asset.Asset,
			gkr asset.GroupKeyRevealV1) wire.TxWitness {

			controlBlock, err := gkr.ScriptSpendControlBlock(
				a.ID(),
			).Unpack()
			require.NoError(t, err)

			controlBlock.InclusionProof = bytes.Join([][]byte{
				fn.ByteSlice(schnorrSigLeaf.TapHash()),
				controlBlock.InclusionProof,
			}, nil)
			controlBlockBytes, err := controlBlock.ToBytes()
			require.NoError(t, err)

			// Witness is just the preimage, the script and the
			// control block.
			return wire.TxWitness{
				hashLockPreimage,
				hashLockLeaf.Script,
				controlBlockBytes,
			}
		},
	}, {
		name: "script spend with signature",
		genWitness: func(t *testing.T, a *asset.Asset,
			gkr asset.GroupKeyRevealV1) wire.TxWitness {

			genTx, prevOut, err := txBuilder.BuildGenesisTx(a)
			require.NoError(t, err)

			controlBlock, err := gkr.ScriptSpendControlBlock(
				a.ID(),
			).Unpack()
			require.NoError(t, err)

			controlBlock.InclusionProof = bytes.Join([][]byte{
				fn.ByteSlice(hashLockLeaf.TapHash()),
				controlBlock.InclusionProof,
			}, nil)
			controlBlockBytes, err := controlBlock.ToBytes()
			require.NoError(t, err)

			leafToSign := &psbt.TaprootTapLeafScript{
				ControlBlock: controlBlockBytes,
				Script:       schnorrSigLeaf.Script,
				LeafVersion:  txscript.BaseLeafVersion,
			}

			witness, err := signGroupKeyV1(
				secondKeyDesc, gkr, genTx, prevOut, mockSigner,
				leafToSign,
			)
			require.NoError(t, err)

			return witness
		},
	}}

	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),
			).Unpack()
			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)
			randAsset.GroupKey = &asset.GroupKey{
				RawKey:        internalKeyDesc,
				GroupPubKey:   *groupPubKey,
				TapscriptRoot: groupKeyReveal.TapscriptRoot(),
			}
			randAsset.PrevWitnesses = []asset.Witness{
				{
					PrevID: &asset.PrevID{},
				},
			}

			witness := tc.genWitness(tt, randAsset, groupKeyReveal)
			randAsset.PrevWitnesses[0].TxWitness = witness

			err = txValidator.Execute(
				randAsset, nil, nil, proof.MockChainLookup,
			)
			require.NoError(tt, err)
		})
	}
}

// TestGroupKeyRevealV1WitnessNoScripts tests the key spend path for a group key
// reveal witness if there are no custom scripts.
func TestGroupKeyRevealV1WitnessNoScripts(t *testing.T) {
	var (
		ctx         = context.Background()
		mockKeyRing = tapgarden.NewMockKeyRing()
		mockSigner  = tapgarden.NewMockGenSigner(mockKeyRing)
		txBuilder   = &tapscript.GroupTxBuilder{}
		txValidator = &tap.ValidatorV0{}
	)

	// We expect just one key to be derived from the mock.
	go func() {
		<-mockKeyRing.ReqKeys
	}()

	// The internal key is for the actual internal key of the group.
	internalKeyDesc, err := mockKeyRing.DeriveNextTaprootAssetKey(ctx)
	require.NoError(t, err)

	randAsset := asset.RandAsset(t, asset.Normal)
	genAssetID := randAsset.ID()
	groupKeyReveal, err := asset.NewGroupKeyRevealV1(
		*internalKeyDesc.PubKey, genAssetID, fn.None[chainhash.Hash](),
	).Unpack()
	require.NoError(t, err)

	// Set the group key on the asset, since it's a randomly created group
	// key otherwise.
	groupPubKey, err := groupKeyReveal.GroupPubKey(genAssetID)
	require.NoError(t, err)
	randAsset.GroupKey = &asset.GroupKey{
		RawKey:        internalKeyDesc,
		GroupPubKey:   *groupPubKey,
		TapscriptRoot: groupKeyReveal.TapscriptRoot(),
	}
	randAsset.PrevWitnesses = []asset.Witness{
		{
			PrevID: &asset.PrevID{},
		},
	}

	genTx, prevOut, err := txBuilder.BuildGenesisTx(randAsset)
	require.NoError(t, err)

	witness, err := signGroupKeyV1(
		internalKeyDesc, groupKeyReveal, genTx, prevOut, mockSigner,
		nil,
	)
	require.NoError(t, err)

	randAsset.PrevWitnesses[0].TxWitness = witness

	err = txValidator.Execute(
		randAsset, nil, nil, proof.MockChainLookup,
	)
	require.NoError(t, err)
}

// signGroupKeyV1 is the equivalent for asset.DeriveGroupKey but for a V1 key.
func signGroupKeyV1(keyDesc keychain.KeyDescriptor, gk asset.GroupKeyRevealV1,
	genTx *wire.MsgTx, prevOut *wire.TxOut, signer asset.GenesisSigner,
	leafToSign *psbt.TaprootTapLeafScript) (wire.TxWitness, error) {

	signDesc := &lndclient.SignDescriptor{
		KeyDesc:    keyDesc,
		TapTweak:   gk.TapscriptRoot(),
		Output:     prevOut,
		HashType:   txscript.SigHashDefault,
		InputIndex: 0,
		SignMethod: input.TaprootKeySpendSignMethod,
	}

	if leafToSign != nil {
		signDesc.SignMethod = input.TaprootScriptSpendSignMethod
		signDesc.WitnessScript = leafToSign.Script
	}

	sig, err := signer.SignVirtualTx(signDesc, genTx, prevOut)
	if err != nil {
		return nil, err
	}

	witness := wire.TxWitness{sig.Serialize()}

	// If this was a script spend, we also have to add the script itself and
	// the control block to the witness, otherwise the verifier will reject
	// the generated witness.
	if signDesc.SignMethod == input.TaprootScriptSpendSignMethod &&
		leafToSign != nil {

		witness = append(
			witness, signDesc.WitnessScript,
			leafToSign.ControlBlock,
		)
	}

	return witness, nil
}

Must be added to tapgarden/planter_test.go to work.
A lot of that code will then be useful later on when we implement the actual signing changes in the planter.

Introduce GroupKeyRevealV1, which avoids tweaking the internal key
with the asset ID. Instead, the asset ID is used to tweak the
tapscript tree. This enables an external PSBT signer to sign with
the internal key without requiring knowledge of the tweak.

For more information, see the in-code documentation.
Modify GroupKeyRevealDecoder to handle decoding of both
GroupKeyRevealV0 and GroupKeyRevealV1 formats.
Relocate GroupKeyRevealEncoder and GroupKeyRevealDecoder to the asset
package to allow direct interrogation in unit tests.
Tests also validate spend script control block.
Add unit tests to verify the different possible spend paths for a
group key reveal witness when custom scripts are used.

These tests were authored by @guggero.
Refactored the GroupKeyRevealRecord size calculation to support both
GroupKeyRevealV0 and GroupKeyRevealV1 formats.
@ffranr
Copy link
Contributor Author

ffranr commented Dec 19, 2024

Must be added to tapgarden/planter_test.go to work.

Works great, thank you!

Introduce helper functions to simplify testing by asserting equality
between instances of group key reveal types.
Add a unit test to verify that encoding and decoding work correctly
when the proof group key reveal is of version 1 type.
@ffranr ffranr force-pushed the add-new-groupkeyreveal branch from c04fbbb to 0318fad Compare December 19, 2024 02:39
@ffranr
Copy link
Contributor Author

ffranr commented Dec 19, 2024

I've added a small unit test which covers Proof encoding/decoding with GroupKeyRevealV1.

I think I've addressed all review comments.

@ffranr ffranr requested review from Roasbeef and guggero December 19, 2024 02:41
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 of 3 files at r1, 3 of 3 files at r2, 1 of 1 files at r3, 1 of 2 files at r5, 2 of 4 files at r6, 3 of 3 files at r7, 2 of 2 files at r8, all commit messages.
Reviewable status: all files reviewed, 34 unresolved discussions (waiting on @ffranr and @guggero)

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🧞‍♂️

Copy link
Member

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work, LGTM 🎉

@guggero guggero merged commit 1ad2399 into main Dec 19, 2024
18 checks passed
@guggero guggero deleted the add-new-groupkeyreveal branch December 19, 2024 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

5 participants