Skip to content

Commit 55a5adc

Browse files
jharveybguggero
authored andcommitted
proof+tappsbt: add STXOProofs to CommitmentProof
This commit adds a new field to the CommitmentProof struct that will house STXO proofs. We also add encoding/decoding functions for that new sub type and update the test vectors accordingly.
1 parent 0631116 commit 55a5adc

File tree

7 files changed

+288
-6
lines changed

7 files changed

+288
-6
lines changed

proof/encoding.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/btcsuite/btcd/btcec/v2"
1212
"github.com/btcsuite/btcd/wire"
1313
"github.com/lightninglabs/taproot-assets/asset"
14+
"github.com/lightninglabs/taproot-assets/commitment"
1415
"github.com/lightninglabs/taproot-assets/fn"
1516
"github.com/lightningnetwork/lnd/tlv"
1617
)
@@ -346,6 +347,102 @@ func CommitmentProofDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error
346347
return tlv.NewTypeForEncodingErr(val, "*CommitmentProof")
347348
}
348349

350+
func CommitmentProofsEncoder(w io.Writer, val any, buf *[8]byte) error {
351+
if t, ok := val.(*map[asset.SerializedKey]commitment.Proof); ok {
352+
numProofs := uint64(len(*t))
353+
if err := tlv.WriteVarInt(w, numProofs, buf); err != nil {
354+
return err
355+
}
356+
357+
var proofBuf bytes.Buffer
358+
for key, proof := range *t {
359+
var keyBytes [33]byte
360+
copy(keyBytes[:], key[:])
361+
362+
err := tlv.EBytes33(w, &keyBytes, buf)
363+
if err != nil {
364+
return err
365+
}
366+
367+
if err := proof.Encode(&proofBuf); err != nil {
368+
return err
369+
}
370+
371+
proofBytes := proofBuf.Bytes()
372+
err = asset.InlineVarBytesEncoder(w, &proofBytes, buf)
373+
if err != nil {
374+
return err
375+
}
376+
377+
proofBuf.Reset()
378+
}
379+
return nil
380+
}
381+
382+
return tlv.NewTypeForEncodingErr(
383+
val, "map[asset.SerializedKey]CommitmentProof",
384+
)
385+
}
386+
387+
func CommitmentProofsDecoder(r io.Reader, val any, buf *[8]byte,
388+
_ uint64) error {
389+
390+
if typ, ok := val.(*map[asset.SerializedKey]commitment.Proof); ok {
391+
numProofs, err := tlv.ReadVarInt(r, buf)
392+
if err != nil {
393+
return err
394+
}
395+
396+
// Avoid OOM by limiting the number of commitment proofs we
397+
// accept.
398+
if numProofs > MaxNumTaprootProofs {
399+
return fmt.Errorf("%w: too many commitment proofs",
400+
ErrProofInvalid)
401+
}
402+
403+
proofs := make(
404+
map[asset.SerializedKey]commitment.Proof, numProofs,
405+
)
406+
for i := uint64(0); i < numProofs; i++ {
407+
var keyBytes [33]byte
408+
409+
err := tlv.DBytes33(
410+
r, &keyBytes, buf,
411+
btcec.PubKeyBytesLenCompressed,
412+
)
413+
if err != nil {
414+
return err
415+
}
416+
417+
var proofBytes []byte
418+
err = asset.InlineVarBytesDecoder(
419+
r, &proofBytes, buf, MaxTaprootProofSizeBytes,
420+
)
421+
if err != nil {
422+
return err
423+
}
424+
425+
var key asset.SerializedKey
426+
copy(key[:], keyBytes[:])
427+
428+
var proof commitment.Proof
429+
err = proof.Decode(bytes.NewReader(proofBytes))
430+
if err != nil {
431+
return err
432+
}
433+
434+
proofs[key] = proof
435+
}
436+
437+
*typ = proofs
438+
return nil
439+
}
440+
441+
return tlv.NewTypeForEncodingErr(
442+
val, "map[asset.SerializedKey]CommitmentProof",
443+
)
444+
}
445+
349446
func TapscriptProofEncoder(w io.Writer, val any, buf *[8]byte) error {
350447
if t, ok := val.(**TapscriptProof); ok {
351448
return (*t).Encode(w)

proof/encoding_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package proof
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/lightninglabs/taproot-assets/asset"
8+
"github.com/lightninglabs/taproot-assets/commitment"
9+
"github.com/lightninglabs/taproot-assets/internal/test"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestCommitmentProofsDecoderRoundTrip(t *testing.T) {
14+
t.Parallel()
15+
16+
testBlocks := readTestData(t)
17+
oddTxBlock := testBlocks[0]
18+
19+
numProofs := 4
20+
proofs := make(map[asset.SerializedKey]commitment.Proof, numProofs)
21+
for range numProofs {
22+
genesis := asset.RandGenesis(t, asset.Collectible)
23+
scriptKey := test.RandPubKey(t)
24+
randProof := RandProof(t, genesis, scriptKey, oddTxBlock, 0, 1)
25+
randCommitmentProof := randProof.InclusionProof.CommitmentProof
26+
serializedKey := asset.SerializedKey(
27+
test.RandPubKey(t).SerializeCompressed(),
28+
)
29+
proofs[serializedKey] = randCommitmentProof.Proof
30+
}
31+
32+
var buf [8]byte
33+
34+
// Helper function to encode a map of commitment proofs.
35+
encodeProofs := func(
36+
proofs map[asset.SerializedKey]commitment.Proof) []byte {
37+
38+
var b bytes.Buffer
39+
err := CommitmentProofsEncoder(&b, &proofs, &buf)
40+
require.NoError(t, err)
41+
return b.Bytes()
42+
}
43+
44+
// Helper function to decode a map of commitment proofs.
45+
decodeProofs := func(
46+
encoded []byte) map[asset.SerializedKey]commitment.Proof {
47+
48+
var decodedProofs map[asset.SerializedKey]commitment.Proof
49+
err := CommitmentProofsDecoder(
50+
bytes.NewReader(encoded), &decodedProofs, &buf,
51+
uint64(len(encoded)),
52+
)
53+
require.NoError(t, err)
54+
return decodedProofs
55+
}
56+
57+
// Test case: round trip encoding and decoding.
58+
t.Run(
59+
"encode and decode map of 4 random commitment proofs",
60+
func(t *testing.T) {
61+
// Encode the proofs.
62+
encoded := encodeProofs(proofs)
63+
64+
// Decode the proofs.
65+
decodedProofs := decodeProofs(encoded)
66+
67+
// Assert the decoded proofs match the original.
68+
require.Equal(t, proofs, decodedProofs)
69+
},
70+
)
71+
72+
// Test case: empty map.
73+
t.Run(
74+
"encode and decode empty map of commitment proofs",
75+
func(t *testing.T) {
76+
// Create an empty map of commitment emptyMap.
77+
emptyMap := map[asset.SerializedKey]commitment.Proof{}
78+
79+
// Encode the proofs.
80+
encoded := encodeProofs(emptyMap)
81+
82+
// Decode the proofs.
83+
decodedMap := decodeProofs(encoded)
84+
85+
// Assert the decoded proofs match the original.
86+
require.Equal(t, emptyMap, decodedMap)
87+
},
88+
)
89+
}

proof/mock.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -872,28 +872,58 @@ func NewTestFromCommitmentProof(t testing.TB,
872872
TapscriptSibling: commitment.HexTapscriptSibling(
873873
t, p.TapSiblingPreimage,
874874
),
875+
STXOProofs: NewTestFromSTXOProofs(t, p),
875876
UnknownOddTypes: p.UnknownOddTypes,
876877
}
877878
}
878879

880+
func NewTestFromSTXOProofs(t testing.TB,
881+
p *CommitmentProof) *map[string]commitment.TestProof {
882+
883+
t.Helper()
884+
885+
stxoProofs := make(map[string]commitment.TestProof)
886+
for key, proof := range p.STXOProofs {
887+
keyHex := hex.EncodeToString(key[:])
888+
stxoProofs[keyHex] = *commitment.NewTestFromProof(t, &proof)
889+
}
890+
return &stxoProofs
891+
}
892+
893+
// nolint: lll
879894
type TestCommitmentProof struct {
880-
Proof *commitment.TestProof `json:"proof"`
881-
TapscriptSibling string `json:"tapscript_sibling"`
882-
UnknownOddTypes tlv.TypeMap `json:"unknown_odd_types"`
895+
Proof *commitment.TestProof `json:"proof"`
896+
TapscriptSibling string `json:"tapscript_sibling"`
897+
STXOProofs *map[string]commitment.TestProof `json:"stxo_proofs"`
898+
UnknownOddTypes tlv.TypeMap `json:"unknown_odd_types"`
883899
}
884900

885901
func (tcp *TestCommitmentProof) ToCommitmentProof(
886902
t testing.TB) *CommitmentProof {
887903

888904
t.Helper()
889905

890-
return &CommitmentProof{
906+
stxoProofs := make(map[asset.SerializedKey]commitment.Proof)
907+
for key, proof := range *tcp.STXOProofs {
908+
keyBytes, err := hex.DecodeString(key)
909+
require.NoError(t, err)
910+
key := asset.SerializedKey(keyBytes)
911+
stxoProofs[key] = *proof.ToProof(t)
912+
}
913+
914+
cp := &CommitmentProof{
891915
Proof: *tcp.Proof.ToProof(t),
892916
TapSiblingPreimage: commitment.ParseTapscriptSibling(
893917
t, tcp.TapscriptSibling,
894918
),
895919
UnknownOddTypes: tcp.UnknownOddTypes,
896920
}
921+
922+
if len(stxoProofs) > 0 {
923+
cp.STXOProofs = stxoProofs
924+
}
925+
926+
return cp
897927
}
898928

899929
func NewTestFromTapscriptProof(t testing.TB,

proof/records.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const (
4040
// count from where commitment.ProofTaprootAssetProofType left off.
4141
CommitmentProofTapSiblingPreimageType tlv.Type = 5
4242

43+
// CommitmentProofSTXOProofsType is the type of the TLV record for the
44+
// Exclusion proof CommitmentProof's STXOProofs field.
45+
CommitmentProofSTXOProofsType tlv.Type = 7
46+
4347
TapscriptProofTapPreimage1 tlv.Type = 1
4448
TapscriptProofTapPreimage2 tlv.Type = 3
4549
TapscriptProofBip86 tlv.Type = 4
@@ -74,7 +78,7 @@ var KnownTaprootProofTypes = fn.NewSet(
7478
// tests.
7579
var KnownCommitmentProofTypes = fn.NewSet(
7680
commitment.ProofAssetProofType, commitment.ProofTaprootAssetProofType,
77-
CommitmentProofTapSiblingPreimageType,
81+
CommitmentProofTapSiblingPreimageType, CommitmentProofSTXOProofsType,
7882
)
7983

8084
// KnownTapscriptProofTypes is a set of all known Tapscript proof TLV types.
@@ -288,6 +292,26 @@ func CommitmentProofTapSiblingPreimageRecord(
288292
)
289293
}
290294

295+
func CommitmentProofSTXOProofsRecord(
296+
stxoProofs *map[asset.SerializedKey]commitment.Proof) tlv.Record {
297+
298+
sizeFunc := func() uint64 {
299+
var buf bytes.Buffer
300+
err := CommitmentProofsEncoder(
301+
&buf, stxoProofs, &[8]byte{},
302+
)
303+
if err != nil {
304+
panic(err)
305+
}
306+
return uint64(len(buf.Bytes()))
307+
}
308+
return tlv.MakeDynamicRecord(
309+
CommitmentProofSTXOProofsType, stxoProofs, sizeFunc,
310+
CommitmentProofsEncoder,
311+
CommitmentProofsDecoder,
312+
)
313+
}
314+
291315
func TapscriptProofTapPreimage1Record(
292316
preimage **commitment.TapscriptPreimage) tlv.Record {
293317

proof/taproot.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ type CommitmentProof struct {
3838
// at the tapscript root of the expected output.
3939
TapSiblingPreimage *commitment.TapscriptPreimage
4040

41+
// STXOProofs is a list of proofs that either prove the spend of the
42+
// inputs as referenced in the asset's previous witnesses' prevIDs when
43+
// this is in an inclusion proof, or the non-spend of an input if this
44+
// is in an exclusion proof. Only the root assets in a transfer (so no
45+
// minted or split assets) will have STXO inclusion or exclusion proofs.
46+
STXOProofs map[asset.SerializedKey]commitment.Proof
47+
4148
// UnknownOddTypes is a map of unknown odd types that were encountered
4249
// during decoding. This map is used to preserve unknown types that we
4350
// don't know of yet, so we can still encode them back when serializing.
@@ -57,6 +64,11 @@ func (p CommitmentProof) EncodeRecords() []tlv.Record {
5764
),
5865
)
5966
}
67+
if len(p.STXOProofs) > 0 {
68+
records = append(records, CommitmentProofSTXOProofsRecord(
69+
&p.STXOProofs,
70+
))
71+
}
6072

6173
// Add any unknown odd types that were encountered during decoding.
6274
return asset.CombineRecords(records, p.UnknownOddTypes)
@@ -65,10 +77,14 @@ func (p CommitmentProof) EncodeRecords() []tlv.Record {
6577
// DecodeRecords returns the decoding records for the CommitmentProof.
6678
func (p *CommitmentProof) DecodeRecords() []tlv.Record {
6779
records := p.Proof.DecodeRecords()
68-
return append(
80+
records = append(
6981
records,
7082
CommitmentProofTapSiblingPreimageRecord(&p.TapSiblingPreimage),
7183
)
84+
return append(
85+
records,
86+
CommitmentProofSTXOProofsRecord(&p.STXOProofs),
87+
)
7288
}
7389

7490
// Encode attempts to encode the CommitmentProof into the passed io.Writer.

0 commit comments

Comments
 (0)