Skip to content

Commit f0907b5

Browse files
authored
Merge pull request #1180 from lightninglabs/psbt_aux_leaves
tappsbt: add AltLeaf support to vPacket
2 parents a17ea40 + fda8a6d commit f0907b5

35 files changed

+14942
-235
lines changed

address/mock.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func RandAddr(t testing.TB, params *ChainParams,
3333
proofCourierAddr url.URL) (*AddrWithKeyInfo,
3434
*asset.Genesis, *asset.GroupKey) {
3535

36-
scriptKeyPriv := test.RandPrivKey(t)
36+
scriptKeyPriv := test.RandPrivKey()
3737
scriptKey := asset.NewScriptKeyBip86(keychain.KeyDescriptor{
3838
PubKey: scriptKeyPriv.PubKey(),
3939
KeyLocator: keychain.KeyLocator{
@@ -42,7 +42,7 @@ func RandAddr(t testing.TB, params *ChainParams,
4242
},
4343
})
4444

45-
internalKey := test.RandPrivKey(t)
45+
internalKey := test.RandPrivKey()
4646

4747
genesis := asset.RandGenesis(t, asset.Type(test.RandInt31n(2)))
4848
amount := test.RandInt[uint64]()

asset/asset.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ var (
106106
// asset split leaves.
107107
ZeroPrevID PrevID
108108

109+
// EmptyGenesis is the empty Genesis struct used for alt leaves.
110+
EmptyGenesis Genesis
111+
109112
// NUMSBytes is the NUMs point we'll use for un-spendable script keys.
110113
// It was generated via a try-and-increment approach using the phrase
111114
// "taproot-assets" with SHA2-256. The code for the try-and-increment
@@ -2238,3 +2241,137 @@ type ChainAsset struct {
22382241
// available for coin selection.
22392242
AnchorLeaseExpiry *time.Time
22402243
}
2244+
2245+
// An AltLeaf is a type that is used to carry arbitrary data, and does not
2246+
// represent a Taproot asset. An AltLeaf can be used to anchor other protocols
2247+
// alongside Taproot Asset transactions.
2248+
type AltLeaf[T any] interface {
2249+
// Copyable asserts that the target type of this interface satisfies
2250+
// the Copyable interface.
2251+
fn.Copyable[T]
2252+
2253+
// ValidateAltLeaf ensures that an AltLeaf is valid.
2254+
ValidateAltLeaf() error
2255+
2256+
// EncodeAltLeaf encodes an AltLeaf into a TLV stream.
2257+
EncodeAltLeaf(w io.Writer) error
2258+
2259+
// DecodeAltLeaf decodes an AltLeaf from a TLV stream.
2260+
DecodeAltLeaf(r io.Reader) error
2261+
}
2262+
2263+
// NewAltLeaf instantiates a new valid AltLeaf.
2264+
func NewAltLeaf(key ScriptKey, keyVersion ScriptVersion,
2265+
prevWitness []Witness) (*Asset, error) {
2266+
2267+
if key.PubKey == nil {
2268+
return nil, fmt.Errorf("script key must be non-nil")
2269+
}
2270+
2271+
return &Asset{
2272+
Version: V0,
2273+
Genesis: EmptyGenesis,
2274+
Amount: 0,
2275+
LockTime: 0,
2276+
RelativeLockTime: 0,
2277+
PrevWitnesses: prevWitness,
2278+
SplitCommitmentRoot: nil,
2279+
GroupKey: nil,
2280+
ScriptKey: key,
2281+
ScriptVersion: keyVersion,
2282+
}, nil
2283+
}
2284+
2285+
// CopyAltLeaf performs a deep copy of an AltLeaf.
2286+
func CopyAltLeaf[T AltLeaf[T]](a AltLeaf[T]) AltLeaf[T] {
2287+
return a.Copy()
2288+
}
2289+
2290+
// CopyAltLeaves performs a deep copy of an AltLeaf slice.
2291+
func CopyAltLeaves[T AltLeaf[T]](a []AltLeaf[T]) []AltLeaf[T] {
2292+
return fn.Map(a, CopyAltLeaf[T])
2293+
}
2294+
2295+
// Validate checks that an Asset is a valid AltLeaf. An Asset used as an AltLeaf
2296+
// must meet these constraints:
2297+
// - Version must be V0.
2298+
// - Genesis must be the empty Genesis.
2299+
// - Amount, LockTime, and RelativeLockTime must be 0.
2300+
// - SplitCommitmentRoot and GroupKey must be nil.
2301+
// - ScriptKey must be non-nil.
2302+
func (a *Asset) ValidateAltLeaf() error {
2303+
if a.Version != V0 {
2304+
return fmt.Errorf("alt leaf version must be 0")
2305+
}
2306+
2307+
if a.Genesis != EmptyGenesis {
2308+
return fmt.Errorf("alt leaf genesis must be the empty genesis")
2309+
}
2310+
2311+
if a.Amount != 0 {
2312+
return fmt.Errorf("alt leaf amount must be 0")
2313+
}
2314+
2315+
if a.LockTime != 0 {
2316+
return fmt.Errorf("alt leaf lock time must be 0")
2317+
}
2318+
2319+
if a.RelativeLockTime != 0 {
2320+
return fmt.Errorf("alt leaf relative lock time must be 0")
2321+
}
2322+
2323+
if a.SplitCommitmentRoot != nil {
2324+
return fmt.Errorf(
2325+
"alt leaf split commitment root must be empty",
2326+
)
2327+
}
2328+
2329+
if a.GroupKey != nil {
2330+
return fmt.Errorf("alt leaf group key must be empty")
2331+
}
2332+
2333+
if a.ScriptKey.PubKey == nil {
2334+
return fmt.Errorf("alt leaf script key must be non-nil")
2335+
}
2336+
2337+
return nil
2338+
}
2339+
2340+
// encodeAltLeafRecords determines the set of non-nil records to include when
2341+
// encoding an AltLeaf. Since the Genesis, Group Key, Amount, and Version fields
2342+
// are static, we can omit those fields.
2343+
func (a *Asset) encodeAltLeafRecords() []tlv.Record {
2344+
records := make([]tlv.Record, 0, 3)
2345+
2346+
// Always use the normal witness encoding, since the asset version is
2347+
// always V0.
2348+
if len(a.PrevWitnesses) > 0 {
2349+
records = append(records, NewLeafPrevWitnessRecord(
2350+
&a.PrevWitnesses, EncodeNormal,
2351+
))
2352+
}
2353+
records = append(records, NewLeafScriptVersionRecord(&a.ScriptVersion))
2354+
records = append(records, NewLeafScriptKeyRecord(&a.ScriptKey.PubKey))
2355+
2356+
// Add any unknown odd types that were encountered during decoding.
2357+
return CombineRecords(records, a.UnknownOddTypes)
2358+
}
2359+
2360+
// EncodeAltLeaf encodes an AltLeaf into a TLV stream.
2361+
func (a *Asset) EncodeAltLeaf(w io.Writer) error {
2362+
stream, err := tlv.NewStream(a.encodeAltLeafRecords()...)
2363+
if err != nil {
2364+
return err
2365+
}
2366+
return stream.Encode(w)
2367+
}
2368+
2369+
// DecodeAltLeaf decodes an AltLeaf from a TLV stream. The normal Asset decoder
2370+
// can be reused here, since any Asset field not encoded in the AltLeaf will
2371+
// be set to its default value, which matches the AltLeaf validity constraints.
2372+
func (a *Asset) DecodeAltLeaf(r io.Reader) error {
2373+
return a.Decode(r)
2374+
}
2375+
2376+
// Ensure Asset implements the AltLeaf interface.
2377+
var _ AltLeaf[*Asset] = (*Asset)(nil)

asset/asset_test.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"crypto/sha256"
66
"encoding/hex"
7+
"reflect"
78
"testing"
89

910
"github.com/btcsuite/btcd/blockchain"
@@ -18,6 +19,7 @@ import (
1819
"github.com/lightningnetwork/lnd/keychain"
1920
"github.com/lightningnetwork/lnd/tlv"
2021
"github.com/stretchr/testify/require"
22+
"pgregory.net/rapid"
2123
)
2224

2325
var (
@@ -514,6 +516,79 @@ func TestAssetEncoding(t *testing.T) {
514516
test.WriteTestVectors(t, generatedTestVectorName, testVectors)
515517
}
516518

519+
// TestAltLeafEncoding runs a property test for AltLeaf validation, encoding,
520+
// and decoding.
521+
func TestAltLeafEncoding(t *testing.T) {
522+
t.Run("alt leaf encode/decode", rapid.MakeCheck(testAltLeafEncoding))
523+
}
524+
525+
// testAltLeafEncoding tests the AltLeaf validation logic, and that a valid
526+
// AltLeaf can be encoded and decoded correctly.
527+
func testAltLeafEncoding(t *rapid.T) {
528+
protoLeaf := AltLeafGen(t).Draw(t, "alt_leaf")
529+
validAltLeafErr := protoLeaf.ValidateAltLeaf()
530+
531+
// If validation passes, the asset must follow all alt leaf constraints.
532+
asserts := []AssetAssert{
533+
AssetVersionAssert(V0),
534+
AssetGenesisAssert(EmptyGenesis),
535+
AssetAmountAssert(0),
536+
AssetLockTimeAssert(0),
537+
AssetRelativeLockTimeAssert(0),
538+
AssetHasSplitRootAssert(false),
539+
AssetGroupKeyAssert(nil),
540+
AssetHasScriptKeyAssert(true),
541+
}
542+
assertErr := CheckAssetAsserts(&protoLeaf, asserts...)
543+
544+
// If the validation method and these assertions behave differently,
545+
// either the test or the validation method is incorrect.
546+
switch {
547+
case validAltLeafErr == nil && assertErr != nil:
548+
t.Error(assertErr)
549+
550+
case validAltLeafErr != nil && assertErr == nil:
551+
t.Error(validAltLeafErr)
552+
553+
default:
554+
}
555+
556+
// Don't test encoding for invalid alt leaves.
557+
if validAltLeafErr != nil {
558+
return
559+
}
560+
561+
// If the alt leaf is valid, check that it can be encoded without error,
562+
// and decoded to an identical alt leaf.
563+
// fmt.Println("valid leaf")
564+
var buf bytes.Buffer
565+
if err := protoLeaf.EncodeAltLeaf(&buf); err != nil {
566+
t.Error(err)
567+
}
568+
569+
var decodedLeaf Asset
570+
altLeafBytes := bytes.NewReader(buf.Bytes())
571+
if err := decodedLeaf.DecodeAltLeaf(altLeafBytes); err != nil {
572+
t.Error(err)
573+
}
574+
575+
if !protoLeaf.DeepEqual(&decodedLeaf) {
576+
t.Errorf("decoded leaf %v does not match input %v", decodedLeaf,
577+
protoLeaf)
578+
}
579+
580+
// Asset.DeepEqual does not inspect UnknownOddTypes, so check for their
581+
// equality separately.
582+
if !reflect.DeepEqual(
583+
protoLeaf.UnknownOddTypes, decodedLeaf.UnknownOddTypes,
584+
) {
585+
586+
t.Errorf("decoded leaf unknown types %v does not match input "+
587+
"%v", decodedLeaf.UnknownOddTypes,
588+
protoLeaf.UnknownOddTypes)
589+
}
590+
}
591+
517592
// TestTapLeafEncoding asserts that we can properly encode and decode tapLeafs
518593
// through their TLV serialization, and that invalid tapLeafs are rejected.
519594
func TestTapLeafEncoding(t *testing.T) {
@@ -857,7 +932,7 @@ func TestAssetGroupKey(t *testing.T) {
857932
func TestDeriveGroupKey(t *testing.T) {
858933
t.Parallel()
859934

860-
groupPriv := test.RandPrivKey(t)
935+
groupPriv := test.RandPrivKey()
861936
groupPub := groupPriv.PubKey()
862937
groupKeyDesc := test.PubToKeyDesc(groupPub)
863938
genSigner := NewMockGenesisSigner(groupPriv)

asset/encoding.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ var (
3232
// ErrByteSliceTooLarge is returned when an encoded byte slice is too
3333
// large.
3434
ErrByteSliceTooLarge = errors.New("bytes: too large")
35+
36+
// ErrDuplicateScriptKeys is returned when two alt leaves have the same
37+
// script key.
38+
ErrDuplicateScriptKeys = errors.New("alt leaf: duplicate script keys")
3539
)
3640

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

804808
return &leaf, nil
805809
}
810+
811+
func AltLeavesEncoder(w io.Writer, val any, buf *[8]byte) error {
812+
if t, ok := val.(*[]AltLeaf[*Asset]); ok {
813+
if err := tlv.WriteVarInt(w, uint64(len(*t)), buf); err != nil {
814+
return err
815+
}
816+
817+
var streamBuf bytes.Buffer
818+
leafKeys := make(map[SerializedKey]struct{})
819+
for _, leaf := range *t {
820+
// Check that this leaf has a unique script key compared
821+
// to all previous leaves. This type assertion is safe
822+
// as we've made an equivalent assertion above.
823+
leafKey := ToSerialized(leaf.(*Asset).ScriptKey.PubKey)
824+
_, ok := leafKeys[leafKey]
825+
if ok {
826+
return fmt.Errorf("%w: %x",
827+
ErrDuplicateScriptKeys, leafKey)
828+
}
829+
830+
leafKeys[leafKey] = struct{}{}
831+
err := leaf.EncodeAltLeaf(&streamBuf)
832+
if err != nil {
833+
return err
834+
}
835+
streamBytes := streamBuf.Bytes()
836+
err = InlineVarBytesEncoder(w, &streamBytes, buf)
837+
if err != nil {
838+
return err
839+
}
840+
841+
streamBuf.Reset()
842+
}
843+
return nil
844+
}
845+
return tlv.NewTypeForEncodingErr(val, "[]AltLeaf")
846+
}
847+
848+
func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
849+
// There is no limit on the number of alt leaves, but the total size of
850+
// all alt leaves must be below 64 KiB.
851+
if l > math.MaxUint16 {
852+
return tlv.ErrRecordTooLarge
853+
}
854+
855+
if typ, ok := val.(*[]AltLeaf[*Asset]); ok {
856+
// Each alt leaf is at least 42 bytes, which limits the total
857+
// number of aux leaves. So we don't need to enforce a strict
858+
// limit here.
859+
numItems, err := tlv.ReadVarInt(r, buf)
860+
if err != nil {
861+
return err
862+
}
863+
864+
leaves := make([]AltLeaf[*Asset], 0, numItems)
865+
leafKeys := make(map[SerializedKey]struct{})
866+
for i := uint64(0); i < numItems; i++ {
867+
var streamBytes []byte
868+
err = InlineVarBytesDecoder(
869+
r, &streamBytes, buf, math.MaxUint16,
870+
)
871+
if err != nil {
872+
return err
873+
}
874+
875+
var leaf Asset
876+
err = leaf.DecodeAltLeaf(bytes.NewReader(streamBytes))
877+
if err != nil {
878+
return err
879+
}
880+
881+
// Check that each alt leaf has a unique script key.
882+
leafKey := ToSerialized(leaf.ScriptKey.PubKey)
883+
_, ok := leafKeys[leafKey]
884+
if ok {
885+
return fmt.Errorf("%w: %x",
886+
ErrDuplicateScriptKeys, leafKey)
887+
}
888+
889+
leafKeys[leafKey] = struct{}{}
890+
leaves = append(leaves, AltLeaf[*Asset](&leaf))
891+
}
892+
893+
*typ = leaves
894+
return nil
895+
}
896+
return tlv.NewTypeForEncodingErr(val, "[]*AltLeaf")
897+
}

0 commit comments

Comments
 (0)