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

tappsbt: add AltLeaf support to vPacket #1180

Merged
merged 11 commits into from
Nov 14, 2024
4 changes: 2 additions & 2 deletions address/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func RandAddr(t testing.TB, params *ChainParams,
proofCourierAddr url.URL) (*AddrWithKeyInfo,
*asset.Genesis, *asset.GroupKey) {

scriptKeyPriv := test.RandPrivKey(t)
scriptKeyPriv := test.RandPrivKey()
scriptKey := asset.NewScriptKeyBip86(keychain.KeyDescriptor{
PubKey: scriptKeyPriv.PubKey(),
KeyLocator: keychain.KeyLocator{
Expand All @@ -42,7 +42,7 @@ func RandAddr(t testing.TB, params *ChainParams,
},
})

internalKey := test.RandPrivKey(t)
internalKey := test.RandPrivKey()

genesis := asset.RandGenesis(t, asset.Type(test.RandInt31n(2)))
amount := test.RandInt[uint64]()
Expand Down
137 changes: 137 additions & 0 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ var (
// asset split leaves.
ZeroPrevID PrevID

// EmptyGenesis is the empty Genesis struct used for alt leaves.
EmptyGenesis Genesis

// 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
Expand Down Expand Up @@ -2238,3 +2241,137 @@ type ChainAsset struct {
// available for coin selection.
AnchorLeaseExpiry *time.Time
}

// An AltLeaf is a type that is used to carry arbitrary data, and does not
// represent a Taproot asset. An AltLeaf can be used to anchor other protocols
// alongside Taproot Asset transactions.
type AltLeaf[T any] interface {
// Copyable asserts that the target type of this interface satisfies
// the Copyable interface.
fn.Copyable[T]

// ValidateAltLeaf ensures that an AltLeaf is valid.
ValidateAltLeaf() error

// EncodeAltLeaf encodes an AltLeaf into a TLV stream.
EncodeAltLeaf(w io.Writer) error

// DecodeAltLeaf decodes an AltLeaf from a TLV stream.
DecodeAltLeaf(r io.Reader) error
}

// NewAltLeaf instantiates a new valid AltLeaf.
func NewAltLeaf(key ScriptKey, keyVersion ScriptVersion,
prevWitness []Witness) (*Asset, error) {

if key.PubKey == nil {
return nil, fmt.Errorf("script key must be non-nil")
}

return &Asset{
Version: V0,
Genesis: EmptyGenesis,
Amount: 0,
LockTime: 0,
RelativeLockTime: 0,
PrevWitnesses: prevWitness,
SplitCommitmentRoot: nil,
GroupKey: nil,
ScriptKey: key,
ScriptVersion: keyVersion,
}, nil
}

// CopyAltLeaf performs a deep copy of an AltLeaf.
func CopyAltLeaf[T AltLeaf[T]](a AltLeaf[T]) AltLeaf[T] {
return a.Copy()
}

// CopyAltLeaves performs a deep copy of an AltLeaf slice.
func CopyAltLeaves[T AltLeaf[T]](a []AltLeaf[T]) []AltLeaf[T] {
return fn.Map(a, CopyAltLeaf[T])
}

// Validate checks that an Asset is a valid AltLeaf. An Asset used as an AltLeaf
// must meet these constraints:
// - Version must be V0.
// - Genesis must be the empty Genesis.
// - Amount, LockTime, and RelativeLockTime must be 0.
// - SplitCommitmentRoot and GroupKey must be nil.
// - ScriptKey must be non-nil.
func (a *Asset) ValidateAltLeaf() error {
if a.Version != V0 {
return fmt.Errorf("alt leaf version must be 0")
}

if a.Genesis != EmptyGenesis {
return fmt.Errorf("alt leaf genesis must be the empty genesis")
}

if a.Amount != 0 {
return fmt.Errorf("alt leaf amount must be 0")
}

if a.LockTime != 0 {
return fmt.Errorf("alt leaf lock time must be 0")
}

if a.RelativeLockTime != 0 {
return fmt.Errorf("alt leaf relative lock time must be 0")
}

if a.SplitCommitmentRoot != nil {
return fmt.Errorf(
"alt leaf split commitment root must be empty",
)
}

if a.GroupKey != nil {
return fmt.Errorf("alt leaf group key must be empty")
}

if a.ScriptKey.PubKey == nil {
return fmt.Errorf("alt leaf script key must be non-nil")
}

return nil
}

// encodeAltLeafRecords determines the set of non-nil records to include when
// encoding an AltLeaf. Since the Genesis, Group Key, Amount, and Version fields
// are static, we can omit those fields.
func (a *Asset) encodeAltLeafRecords() []tlv.Record {
records := make([]tlv.Record, 0, 3)

// Always use the normal witness encoding, since the asset version is
// always V0.
if len(a.PrevWitnesses) > 0 {
Copy link
Member

Choose a reason for hiding this comment

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

Will the alt leaf version ever have a use for the prev witness value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm I don't expect it to, but that's also the only field left here that's 'consensus' wrt. being handled by parts of tapd, compared to the unknown odd field. Which may be useful for future features.

Given the size limits for []AltLeaves I'm not too worried about it causing dysfunction.

records = append(records, NewLeafPrevWitnessRecord(
&a.PrevWitnesses, EncodeNormal,
))
}
records = append(records, NewLeafScriptVersionRecord(&a.ScriptVersion))
records = append(records, NewLeafScriptKeyRecord(&a.ScriptKey.PubKey))

// Add any unknown odd types that were encountered during decoding.
return CombineRecords(records, a.UnknownOddTypes)
}

// EncodeAltLeaf encodes an AltLeaf into a TLV stream.
func (a *Asset) EncodeAltLeaf(w io.Writer) error {
stream, err := tlv.NewStream(a.encodeAltLeafRecords()...)
if err != nil {
return err
}
return stream.Encode(w)
}

// DecodeAltLeaf decodes an AltLeaf from a TLV stream. The normal Asset decoder
// can be reused here, since any Asset field not encoded in the AltLeaf will
// be set to its default value, which matches the AltLeaf validity constraints.
func (a *Asset) DecodeAltLeaf(r io.Reader) error {
return a.Decode(r)
}

// Ensure Asset implements the AltLeaf interface.
var _ AltLeaf[*Asset] = (*Asset)(nil)
77 changes: 76 additions & 1 deletion asset/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto/sha256"
"encoding/hex"
"reflect"
"testing"

"github.com/btcsuite/btcd/blockchain"
Expand All @@ -18,6 +19,7 @@ import (
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)

var (
Expand Down Expand Up @@ -514,6 +516,79 @@ func TestAssetEncoding(t *testing.T) {
test.WriteTestVectors(t, generatedTestVectorName, testVectors)
}

// TestAltLeafEncoding runs a property test for AltLeaf validation, encoding,
// and decoding.
func TestAltLeafEncoding(t *testing.T) {
t.Run("alt leaf encode/decode", rapid.MakeCheck(testAltLeafEncoding))
}

// testAltLeafEncoding tests the AltLeaf validation logic, and that a valid
// AltLeaf can be encoded and decoded correctly.
func testAltLeafEncoding(t *rapid.T) {
jharveyb marked this conversation as resolved.
Show resolved Hide resolved
protoLeaf := AltLeafGen(t).Draw(t, "alt_leaf")
validAltLeafErr := protoLeaf.ValidateAltLeaf()

// If validation passes, the asset must follow all alt leaf constraints.
asserts := []AssetAssert{
AssetVersionAssert(V0),
AssetGenesisAssert(EmptyGenesis),
AssetAmountAssert(0),
AssetLockTimeAssert(0),
AssetRelativeLockTimeAssert(0),
AssetHasSplitRootAssert(false),
AssetGroupKeyAssert(nil),
AssetHasScriptKeyAssert(true),
}
assertErr := CheckAssetAsserts(&protoLeaf, asserts...)

// If the validation method and these assertions behave differently,
// either the test or the validation method is incorrect.
switch {
case validAltLeafErr == nil && assertErr != nil:
t.Error(assertErr)

case validAltLeafErr != nil && assertErr == nil:
t.Error(validAltLeafErr)

default:
}

// Don't test encoding for invalid alt leaves.
if validAltLeafErr != nil {
return
}

// If the alt leaf is valid, check that it can be encoded without error,
// and decoded to an identical alt leaf.
// fmt.Println("valid leaf")
var buf bytes.Buffer
if err := protoLeaf.EncodeAltLeaf(&buf); err != nil {
t.Error(err)
}

var decodedLeaf Asset
altLeafBytes := bytes.NewReader(buf.Bytes())
if err := decodedLeaf.DecodeAltLeaf(altLeafBytes); err != nil {
t.Error(err)
}

if !protoLeaf.DeepEqual(&decodedLeaf) {
t.Errorf("decoded leaf %v does not match input %v", decodedLeaf,
protoLeaf)
}

// Asset.DeepEqual does not inspect UnknownOddTypes, so check for their
// equality separately.
if !reflect.DeepEqual(
protoLeaf.UnknownOddTypes, decodedLeaf.UnknownOddTypes,
) {

t.Errorf("decoded leaf unknown types %v does not match input "+
"%v", decodedLeaf.UnknownOddTypes,
protoLeaf.UnknownOddTypes)
}
}

// TestTapLeafEncoding asserts that we can properly encode and decode tapLeafs
// through their TLV serialization, and that invalid tapLeafs are rejected.
func TestTapLeafEncoding(t *testing.T) {
Expand Down Expand Up @@ -857,7 +932,7 @@ func TestAssetGroupKey(t *testing.T) {
func TestDeriveGroupKey(t *testing.T) {
t.Parallel()

groupPriv := test.RandPrivKey(t)
groupPriv := test.RandPrivKey()
groupPub := groupPriv.PubKey()
groupKeyDesc := test.PubToKeyDesc(groupPub)
genSigner := NewMockGenesisSigner(groupPriv)
Expand Down
92 changes: 92 additions & 0 deletions asset/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ var (
// ErrByteSliceTooLarge is returned when an encoded byte slice is too
// large.
ErrByteSliceTooLarge = errors.New("bytes: too large")

// ErrDuplicateScriptKeys is returned when two alt leaves have the same
// script key.
ErrDuplicateScriptKeys = errors.New("alt leaf: duplicate script keys")
)

func VarIntEncoder(w io.Writer, val any, buf *[8]byte) error {
Expand Down Expand Up @@ -803,3 +807,91 @@ func DecodeTapLeaf(leafData []byte) (*txscript.TapLeaf, error) {

return &leaf, nil
}

func AltLeavesEncoder(w io.Writer, val any, buf *[8]byte) error {
if t, ok := val.(*[]AltLeaf[*Asset]); ok {
if err := tlv.WriteVarInt(w, uint64(len(*t)), buf); err != nil {
return err
}

var streamBuf bytes.Buffer
leafKeys := make(map[SerializedKey]struct{})
for _, leaf := range *t {
// Check that this leaf has a unique script key compared
// to all previous leaves. This type assertion is safe
// as we've made an equivalent assertion above.
leafKey := ToSerialized(leaf.(*Asset).ScriptKey.PubKey)
_, ok := leafKeys[leafKey]
if ok {
return fmt.Errorf("%w: %x",
ErrDuplicateScriptKeys, leafKey)
}

leafKeys[leafKey] = struct{}{}
err := leaf.EncodeAltLeaf(&streamBuf)
if err != nil {
return err
}
streamBytes := streamBuf.Bytes()
err = InlineVarBytesEncoder(w, &streamBytes, buf)
if err != nil {
return err
}

streamBuf.Reset()
}
return nil
}
return tlv.NewTypeForEncodingErr(val, "[]AltLeaf")
}

func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
// There is no limit on the number of alt leaves, but the total size of
// all alt leaves must be below 64 KiB.
if l > math.MaxUint16 {
return tlv.ErrRecordTooLarge
}

if typ, ok := val.(*[]AltLeaf[*Asset]); ok {
// Each alt leaf is at least 42 bytes, which limits the total
guggero marked this conversation as resolved.
Show resolved Hide resolved
// number of aux leaves. So we don't need to enforce a strict
// limit here.
numItems, err := tlv.ReadVarInt(r, buf)
if err != nil {
return err
}

leaves := make([]AltLeaf[*Asset], 0, numItems)
leafKeys := make(map[SerializedKey]struct{})
for i := uint64(0); i < numItems; i++ {
var streamBytes []byte
err = InlineVarBytesDecoder(
r, &streamBytes, buf, math.MaxUint16,
)
if err != nil {
return err
}

var leaf Asset
err = leaf.DecodeAltLeaf(bytes.NewReader(streamBytes))
if err != nil {
return err
}

// Check that each alt leaf has a unique script key.
leafKey := ToSerialized(leaf.ScriptKey.PubKey)
_, ok := leafKeys[leafKey]
if ok {
return fmt.Errorf("%w: %x",
ErrDuplicateScriptKeys, leafKey)
}

leafKeys[leafKey] = struct{}{}
leaves = append(leaves, AltLeaf[*Asset](&leaf))
}

*typ = leaves
return nil
}
return tlv.NewTypeForEncodingErr(val, "[]*AltLeaf")
}
Loading
Loading