Skip to content

Commit

Permalink
Merge pull request #1180 from lightninglabs/psbt_aux_leaves
Browse files Browse the repository at this point in the history
tappsbt: add AltLeaf support to vPacket
  • Loading branch information
Roasbeef authored Nov 14, 2024
2 parents a17ea40 + fda8a6d commit f0907b5
Show file tree
Hide file tree
Showing 35 changed files with 14,942 additions and 235 deletions.
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 {
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) {
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
// 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

0 comments on commit f0907b5

Please sign in to comment.