From e6d65572d625546ad288bd5c14b86d43841358d3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 20 Sep 2023 18:52:22 -0700 Subject: [PATCH] asset: introduce new asset version v1 for segwit-like encoding In this commit, we add a new asset version to introduce segwit-like asset encoding where we don't encode the witness vector of an asset's inputs. This applies on the asset version level, so all v1 asset won't have that vector encoded. Note that this is _only_ for the MS-SMT tree, any other TLV encoding such as the asset proof or state transitions remain the same. Fixes https://github.com/lightninglabs/taproot-assets/issues/464 --- asset/asset.go | 96 ++++++++++++++++++++++++++++++++++++++------- asset/asset_test.go | 31 +++++++++++++++ asset/encoding.go | 42 ++++++++++++++++++-- asset/records.go | 11 ++++-- 4 files changed, 159 insertions(+), 21 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 870ffc9cf..78ef90e68 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -48,6 +48,28 @@ func ToSerialized(pubKey *btcec.PublicKey) SerializedKey { // asset. type Version uint8 +const ( + // V0 is the initial Taproot Asset protocol version. + V0 Version = 0 + + // V1 is the version of asset serialization that doesn't include the + // witness field when creating a TAP commitment. This is similar to + // segwit as found in Bitcoin land. + V1 Version = 1 +) + +// EncodeType is used to denote the type of encoding used for an asset. +type EncodeType uint8 + +const ( + // Encode normal is the normal encoding type for an asset. + EncodeNormal EncodeType = iota + + // EncodeSegwit denotes that the witness vector field is not be be + // encoded. + EncodeSegwit +) + var ( // ZeroPrevID is the blank prev ID used for genesis assets and also // asset split leaves. @@ -79,9 +101,6 @@ const ( // divide that by 2, to allow us to fit this into just a 2-byte integer // and to ensure compatibility with the remote signer. TaprootAssetsKeyFamily = 212 - - // V0 is the initial Taproot Asset protocol version. - V0 Version = 0 ) const ( @@ -320,14 +339,15 @@ type Witness struct { SplitCommitment *SplitCommitment } -// EncodeRecords determines the non-nil records to include when encoding an -// asset witness at runtime. -func (w *Witness) EncodeRecords() []tlv.Record { +// encodeRecords determines the non-nil records to include when encoding an +// asset witness at runtime. This version takes an extra param to determine if +// the witness should be encoded or not. +func (w *Witness) encodeRecords(encodeType EncodeType) []tlv.Record { var records []tlv.Record if w.PrevID != nil { records = append(records, NewWitnessPrevIDRecord(&w.PrevID)) } - if len(w.TxWitness) > 0 { + if len(w.TxWitness) > 0 && encodeType == EncodeNormal { records = append(records, NewWitnessTxWitnessRecord( &w.TxWitness, )) @@ -340,6 +360,12 @@ func (w *Witness) EncodeRecords() []tlv.Record { return records } +// EncodeRecords determines the non-nil records to include when encoding an +// asset witness at runtime. +func (w *Witness) EncodeRecords() []tlv.Record { + return w.encodeRecords(EncodeNormal) +} + // DecodeRecords provides all records known for an asset witness for proper // decoding. func (w *Witness) DecodeRecords() []tlv.Record { @@ -359,6 +385,17 @@ func (w *Witness) Encode(writer io.Writer) error { return stream.Encode(writer) } +// EncodeNoWitness encodes an asset witness into a TLV stream, but does not +// include the raw witness field. The prevID and the split commitment are still +// included. +func (w *Witness) EncodeNoWitness(writer io.Writer) error { + stream, err := tlv.NewStream(w.encodeRecords(EncodeSegwit)...) + if err != nil { + return err + } + return stream.Encode(writer) +} + // Decode decodes an asset witness from a TLV stream. func (w *Witness) Decode(r io.Reader) error { stream, err := tlv.NewStream(w.DecodeRecords()...) @@ -967,9 +1004,9 @@ func (a *Asset) DeepEqual(o *Asset) bool { return true } -// EncodeRecords determines the non-nil records to include when encoding an +// encodeRecords determines the non-nil records to include when encoding an // asset at runtime. -func (a *Asset) EncodeRecords() []tlv.Record { +func (a *Asset) encodeRecords(encodeType EncodeType) []tlv.Record { records := make([]tlv.Record, 0, 11) records = append(records, NewLeafVersionRecord(&a.Version)) records = append(records, NewLeafGenesisRecord(&a.Genesis)) @@ -985,7 +1022,7 @@ func (a *Asset) EncodeRecords() []tlv.Record { } if len(a.PrevWitnesses) > 0 { records = append(records, NewLeafPrevWitnessRecord( - &a.PrevWitnesses, + &a.PrevWitnesses, encodeType, )) } if a.SplitCommitmentRoot != nil { @@ -1001,6 +1038,12 @@ func (a *Asset) EncodeRecords() []tlv.Record { return records } +// EncodeRecords determines the non-nil records to include when encoding an +// asset at runtime. +func (a *Asset) EncodeRecords() []tlv.Record { + return a.encodeRecords(EncodeNormal) +} + // DecodeRecords provides all records known for an asset witness for proper // decoding. func (a *Asset) DecodeRecords() []tlv.Record { @@ -1011,7 +1054,9 @@ func (a *Asset) DecodeRecords() []tlv.Record { NewLeafAmountRecord(&a.Amount), NewLeafLockTimeRecord(&a.LockTime), NewLeafRelativeLockTimeRecord(&a.RelativeLockTime), - NewLeafPrevWitnessRecord(&a.PrevWitnesses), + // We don't need to worry aobut encoding the witness or not + // when we decode, so we just use EncodeNormal here. + NewLeafPrevWitnessRecord(&a.PrevWitnesses, EncodeNormal), NewLeafSplitCommitmentRootRecord(&a.SplitCommitmentRoot), NewLeafScriptVersionRecord(&a.ScriptVersion), NewLeafScriptKeyRecord(&a.ScriptKey.PubKey), @@ -1019,7 +1064,8 @@ func (a *Asset) DecodeRecords() []tlv.Record { } } -// Encode encodes an asset into a TLV stream. +// Encode encodes an asset into a TLV stream. This is used for encoding proof +// files and state transitions. func (a *Asset) Encode(w io.Writer) error { stream, err := tlv.NewStream(a.EncodeRecords()...) if err != nil { @@ -1028,6 +1074,17 @@ func (a *Asset) Encode(w io.Writer) error { return stream.Encode(w) } +// EncodeNoWitness encodes the asset without the witness into a TLV stream. +// This is used for serializing on an asset as a leaf within a TAP MS-SMT tree. +// This only applies when the asset version is v1. +func (a *Asset) EncodeNoWitness(w io.Writer) error { + stream, err := tlv.NewStream(a.encodeRecords(EncodeSegwit)...) + if err != nil { + return err + } + return stream.Encode(w) +} + // Decode decodes an asset from a TLV stream. func (a *Asset) Decode(r io.Reader) error { stream, err := tlv.NewStream(a.DecodeRecords()...) @@ -1040,8 +1097,19 @@ func (a *Asset) Decode(r io.Reader) error { // Leaf returns the asset encoded as a MS-SMT leaf node. func (a *Asset) Leaf() (*mssmt.LeafNode, error) { var buf bytes.Buffer - if err := a.Encode(&buf); err != nil { - return nil, err + + switch a.Version { + case V0: + if err := a.Encode(&buf); err != nil { + return nil, err + } + case V1: + if err := a.EncodeNoWitness(&buf); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unknown asset version: %v", a.Version) } + return mssmt.NewLeafNode(buf.Bytes(), a.Amount), nil } diff --git a/asset/asset_test.go b/asset/asset_test.go index 605a789e7..5a0309e69 100644 --- a/asset/asset_test.go +++ b/asset/asset_test.go @@ -570,3 +570,34 @@ func runBIPTestVector(t *testing.T, testVectors *TestVectors) { }) } } + +// TestAssetEncodingNoWitness tests that we can properly encode and decode an +// asset using the v1 version where the witness is not included. +func TestAssetEncodingNoWitness(t *testing.T) { + t.Parallel() + + // First, start by copying the root asset re-used across tests. + root := testRootAsset.Copy() + + // We'll make another copy that we'll use to modify the witness field. + root2 := root.Copy() + + // We'll now modify the witness field of the second root. + root2.PrevWitnesses[0].TxWitness[0][0] ^= 1 + + // If we encode both of these assets then, then final encoding should + // be identical as we use the EncodeNoWitness method. + var b1, b2 bytes.Buffer + require.NoError(t, root.EncodeNoWitness(&b1)) + require.NoError(t, root2.EncodeNoWitness(&b2)) + + require.Equal(t, b1.Bytes(), b2.Bytes()) + + // The leaf encoding for these two should also be identical. + root1Leaf, err := root.Leaf() + require.NoError(t, err) + root2Leaf, err := root2.Leaf() + require.NoError(t, err) + + require.Equal(t, root1Leaf.NodeHash(), root2Leaf.NodeHash()) +} diff --git a/asset/encoding.go b/asset/encoding.go index ab30d5ec0..83b9d3e0a 100644 --- a/asset/encoding.go +++ b/asset/encoding.go @@ -390,16 +390,48 @@ func TxWitnessDecoder(r io.Reader, val any, buf *[8]byte, _ uint64) error { return tlv.NewTypeForEncodingErr(val, "*wire.TxWitness") } -func WitnessEncoder(w io.Writer, val any, buf *[8]byte) error { +// WitnessEncoderWithType is a wrapper around WitnessEncoder that allows the +// caller to specify th witness type. It's a higher order function that returns +// an encoder function. +func WitnessEncoderWithType(encodeType EncodeType) tlv.Encoder { + return func(w io.Writer, val any, buf *[8]byte) error { + return witnessEncoder(w, val, buf, encodeType) + } +} + +func witnessEncoder(w io.Writer, val any, buf *[8]byte, + encodeType ...EncodeType) error { + if t, ok := val.(*[]Witness); ok { if err := tlv.WriteVarInt(w, uint64(len(*t)), buf); err != nil { return err } for _, assetWitness := range *t { var streamBuf bytes.Buffer - if err := assetWitness.Encode(&streamBuf); err != nil { - return err + switch { + case len(encodeType) == 1 && + encodeType[0] == EncodeSegwit: + + err := assetWitness.EncodeNoWitness(&streamBuf) + if err != nil { + return err + } + + case len(encodeType) == 1 && + encodeType[0] == EncodeNormal: + fallthrough + + case len(encodeType) == 0: + err := assetWitness.Encode(&streamBuf) + if err != nil { + return err + } + + default: + return fmt.Errorf("unknown encode type: %v", + encodeType) } + streamBytes := streamBuf.Bytes() err := VarBytesEncoder(w, &streamBytes, buf) if err != nil { @@ -411,6 +443,10 @@ func WitnessEncoder(w io.Writer, val any, buf *[8]byte) error { return tlv.NewTypeForEncodingErr(val, "[]Witness") } +func WitnessEncoder(w io.Writer, val any, buf *[8]byte) error { + return witnessEncoder(w, val, buf) +} + func WitnessDecoder(r io.Reader, val any, buf *[8]byte, _ uint64) error { if typ, ok := val.(*[]Witness); ok { numItems, err := tlv.ReadVarInt(r, buf) diff --git a/asset/records.go b/asset/records.go index f8bed291b..a9cc2c350 100644 --- a/asset/records.go +++ b/asset/records.go @@ -104,20 +104,23 @@ func NewLeafRelativeLockTimeRecord(relativeLockTime *uint64) tlv.Record { ) } -func NewLeafPrevWitnessRecord(prevWitnesses *[]Witness) tlv.Record { +func NewLeafPrevWitnessRecord(prevWitnesses *[]Witness, + encodeType EncodeType) tlv.Record { + recordSize := func() uint64 { var ( b bytes.Buffer buf [8]byte ) - if err := WitnessEncoder(&b, prevWitnesses, &buf); err != nil { + witnessEncoder := WitnessEncoderWithType(encodeType) + if err := witnessEncoder(&b, prevWitnesses, &buf); err != nil { panic(err) } return uint64(len(b.Bytes())) } return tlv.MakeDynamicRecord( - LeafPrevWitness, prevWitnesses, recordSize, WitnessEncoder, - WitnessDecoder, + LeafPrevWitness, prevWitnesses, recordSize, + WitnessEncoderWithType(encodeType), WitnessDecoder, ) }