Skip to content

Commit 0319458

Browse files
committed
asset: add AltLeaf type and encoder
In this commit, we add the AltLeaf interface, which is used to carry non-Asset data inside a Tap commitment. The Asset type meets this interface, and we add a separate encoder that removes static fields for an Asset being used as an AltLeaf.
1 parent 8e070bd commit 0319458

File tree

2 files changed

+229
-0
lines changed

2 files changed

+229
-0
lines changed

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/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)