Skip to content

Commit

Permalink
asset: introduce new asset version v1 for segwit-like encoding
Browse files Browse the repository at this point in the history
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 #464
  • Loading branch information
Roasbeef committed Sep 23, 2023
1 parent a201d48 commit e6d6557
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 21 deletions.
96 changes: 82 additions & 14 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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,
))
Expand All @@ -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 {
Expand All @@ -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()...)
Expand Down Expand Up @@ -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))
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -1011,15 +1054,18 @@ 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),
NewLeafGroupKeyRecord(&a.GroupKey),
}
}

// 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 {
Expand All @@ -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()...)
Expand All @@ -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
}
31 changes: 31 additions & 0 deletions asset/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
42 changes: 39 additions & 3 deletions asset/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions asset/records.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}

Expand Down

0 comments on commit e6d6557

Please sign in to comment.