From a132dea7b64ca5ec2c1cd4bad27a9acc7e885566 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 7 Feb 2025 16:51:51 +0100 Subject: [PATCH] proof: add universe commitments record types --- proof/encoding.go | 73 +++++++++ proof/records.go | 2 + proof/uni_commitments.go | 283 ++++++++++++++++++++++++++++++++++ proof/uni_commitments_test.go | 146 ++++++++++++++++++ 4 files changed, 504 insertions(+) create mode 100644 proof/uni_commitments.go create mode 100644 proof/uni_commitments_test.go diff --git a/proof/encoding.go b/proof/encoding.go index cb5bcbda0..364b1a2b5 100644 --- a/proof/encoding.go +++ b/proof/encoding.go @@ -642,3 +642,76 @@ func PublicKeyOptionDecoder(r io.Reader, val any, buf *[8]byte, val, "*fn.Option[btcec.PublicKey]", l, l, ) } + +// UniCommitmentVersionEncoder is a function that can be used to encode a +// UniCommitmentVersion to a writer. +func UniCommitmentVersionEncoder(w io.Writer, val any, buf *[8]byte) error { + if version, ok := val.(*UniCommitmentVersion); ok { + versionUint8 := uint8(*version) + return tlv.EUint8(w, &versionUint8, buf) + } + + return tlv.NewTypeForEncodingErr(val, "UniCommitmentVersion") +} + +// UniCommitmentVersionDecoder is a function that can be used to decode a +// UniCommitmentVersion from a reader. +func UniCommitmentVersionDecoder(r io.Reader, val any, buf *[8]byte, + l uint64) error { + + if version, ok := val.(*UniCommitmentVersion); ok { + var versionInt uint8 + err := tlv.DUint8(r, &versionInt, buf, l) + if err != nil { + return err + } + + *version = UniCommitmentVersion(versionInt) + return nil + } + + return tlv.NewTypeForDecodingErr(val, "UniCommitmentVersion", l, 1) +} + +// UniCommitmentParamsEncoder is a function that can be used to encode a +// UniCommitmentParams to a writer. +func UniCommitmentParamsEncoder(w io.Writer, val any, buf *[8]byte) error { + if t, ok := val.(*UniCommitmentParams); ok { + var paramsBuf bytes.Buffer + if err := t.Encode(¶msBuf); err != nil { + return err + } + + paramsBytes := paramsBuf.Bytes() + return asset.InlineVarBytesEncoder(w, ¶msBytes, buf) + } + + return tlv.NewTypeForEncodingErr(val, "UniCommitmentParams") +} + +// UniCommitmentParamsDecoder is a function that can be used to decode a +// UniCommitmentParams from a reader. +func UniCommitmentParamsDecoder(r io.Reader, val any, buf *[8]byte, + l uint64) error { + + if typ, ok := val.(*UniCommitmentParams); ok { + var paramsBytes []byte + err := asset.InlineVarBytesDecoder( + r, ¶msBytes, buf, tlv.MaxRecordSize, + ) + if err != nil { + return err + } + + var params UniCommitmentParams + err = params.Decode(bytes.NewReader(paramsBytes)) + if err != nil { + return err + } + + *typ = params + return nil + } + + return tlv.NewTypeForDecodingErr(val, "UniCommitmentParams", l, 1) +} diff --git a/proof/records.go b/proof/records.go index 4a882f719..64fdde26a 100644 --- a/proof/records.go +++ b/proof/records.go @@ -29,6 +29,7 @@ const ( GenesisRevealType tlv.Type = 23 GroupKeyRevealType tlv.Type = 25 AltLeavesType tlv.Type = 27 + UniCommitmentsType tlv.Type = 29 TaprootProofOutputIndexType tlv.Type = 0 TaprootProofInternalKeyType tlv.Type = 2 @@ -60,6 +61,7 @@ var KnownProofTypes = fn.NewSet( ExclusionProofsType, SplitRootProofType, MetaRevealType, AdditionalInputsType, ChallengeWitnessType, BlockHeightType, GenesisRevealType, GroupKeyRevealType, AltLeavesType, + UniCommitmentsType, ) // KnownTaprootProofTypes is a set of all known Taproot proof TLV types. This diff --git a/proof/uni_commitments.go b/proof/uni_commitments.go new file mode 100644 index 000000000..f0a14f046 --- /dev/null +++ b/proof/uni_commitments.go @@ -0,0 +1,283 @@ +package proof + +import ( + "bytes" + "context" + "crypto/sha256" + "errors" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +var ( + // ErrSignatureVerificationFailed is an error that is returned when the + // signature verification of the UniCommitments fails. + ErrSignatureVerificationFailed = errors.New( + "signature verification failed", + ) +) + +// UniCommitmentVersion is a type that represents the version of the +// UniCommitments. +type UniCommitmentVersion uint8 + +const ( + // UniCommitmentV0 is the first version of the UniCommitments. + UniCommitmentV0 UniCommitmentVersion = 0 +) + +// Record returns a TLV record that can be used to encode/decode a +// UniCommitmentVersion to/from a TLV stream. +func (v *UniCommitmentVersion) Record() tlv.Record { + // We set the type to zero here because the type parameter in + // tlv.RecordT will be used as the actual type. + return tlv.MakeStaticRecord( + 0, v, 1, UniCommitmentVersionEncoder, + UniCommitmentVersionDecoder, + ) +} + +// UniCommitmentParams is a struct that holds the parameters for universe +// commitments. +type UniCommitmentParams struct { + Version tlv.RecordT[tlv.TlvType0, UniCommitmentVersion] + PreCommitmentIndex tlv.RecordT[tlv.TlvType2, uint32] +} + +// NewUniCommitmentParams creates a new UniCommitmentParams instance with the +// given version and pre-commitment index. +func NewUniCommitmentParams(version UniCommitmentVersion, + preCommitmentIndex uint32) *UniCommitmentParams { + + return &UniCommitmentParams{ + Version: tlv.NewRecordT[tlv.TlvType0](version), + PreCommitmentIndex: tlv.NewPrimitiveRecord[tlv.TlvType2]( + preCommitmentIndex, + ), + } +} + +// Encode serializes the UniCommitmentParams to the given io.Writer. +func (p *UniCommitmentParams) Encode(w io.Writer) error { + records := []tlv.Record{ + p.Version.Record(), + p.PreCommitmentIndex.Record(), + } + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// Decode deserializes the UniCommitmentParams from the given io.Reader. +func (p *UniCommitmentParams) Decode(r io.Reader) error { + tlvStream, err := tlv.NewStream( + p.Version.Record(), + p.PreCommitmentIndex.Record(), + ) + if err != nil { + return err + } + + return tlvStream.DecodeP2P(r) +} + +// Bytes returns the serialized UniCommitmentParams record. +func (p *UniCommitmentParams) Bytes() []byte { + var buf bytes.Buffer + _ = p.Encode(&buf) + return buf.Bytes() +} + +// Record creates a Record out of a UniCommitmentParams. +// +// NOTE: This is part of the tlv.RecordProducer interface. +func (p *UniCommitmentParams) Record() tlv.Record { + size := func() uint64 { + var ( + buf bytes.Buffer + scratch [8]byte + ) + err := UniCommitmentParamsEncoder(&buf, p, &scratch) + if err != nil { + panic(err) + } + + return uint64(buf.Len()) + } + return tlv.MakeDynamicRecord( + 0, p, size, UniCommitmentParamsEncoder, + UniCommitmentParamsDecoder, + ) +} + +// UniCommitments is a struct that holds the universe commitment parameters and +// the authorized signature over those parameters. +type UniCommitments struct { + Params tlv.OptionalRecordT[tlv.TlvType0, UniCommitmentParams] + Sig tlv.RecordT[tlv.TlvType2, lnwire.Sig] +} + +// NewUniCommitments creates a new UniCommitments instance with the given +// parameters and signature. +func NewUniCommitments(params *UniCommitmentParams, + sig lnwire.Sig) *UniCommitments { + + var paramsRecord tlv.OptionalRecordT[tlv.TlvType0, UniCommitmentParams] + if params != nil { + paramsRecord = tlv.SomeRecordT[tlv.TlvType0]( + tlv.NewRecordT[tlv.TlvType0](*params), + ) + } + + return &UniCommitments{ + Params: paramsRecord, + Sig: tlv.NewRecordT[tlv.TlvType2](sig), + } +} + +// Encode serializes the UniCommitments to the given io.Writer. +func (p *UniCommitments) Encode(w io.Writer) error { + records := []tlv.Record{ + p.Sig.Record(), + } + + p.Params.WhenSome( + func(r tlv.RecordT[tlv.TlvType0, UniCommitmentParams]) { + records = append(records, r.Record()) + }, + ) + + tlv.SortRecords(records) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// Decode deserializes the UniCommitments from the given io.Reader. +func (p *UniCommitments) Decode(r io.Reader) error { + params := p.Params.Zero() + + tlvStream, err := tlv.NewStream( + params.Record(), + p.Sig.Record(), + ) + if err != nil { + return err + } + + tlvs, err := tlvStream.DecodeWithParsedTypesP2P(r) + if err != nil { + return err + } + + if _, ok := tlvs[params.TlvType()]; ok { + p.Params = tlv.SomeRecordT(params) + } + + // We need to force the signature type to be a Schnorr signature for the + // unit tests to pass. + var emptySig lnwire.Sig + if p.Sig.Val != emptySig { + p.Sig.Val.ForceSchnorr() + } + + return nil +} + +// Bytes returns the serialized UniCommitments record. +func (p *UniCommitments) Bytes() []byte { + var buf bytes.Buffer + _ = p.Encode(&buf) + return buf.Bytes() +} + +// VerificationDigest returns the digest of the UniCommitments that should be +// used for verification of the signature. +func (p *UniCommitments) VerificationDigest(mintPoint wire.OutPoint) [32]byte { + hash := sha256.New() + _ = wire.WriteOutPoint(hash, 0, 0, &mintPoint) + p.Params.ValOpt().WhenSome(func(params UniCommitmentParams) { + _ = params.Encode(hash) + }) + + return ([32]byte)(hash.Sum(nil)) +} + +// Record creates a Record out of a UniCommitments. +// +// NOTE: This is part of the tlv.RecordProducer interface. +func (p *UniCommitments) Record() tlv.Record { + size := func() uint64 { + var ( + buf bytes.Buffer + scratch [8]byte + ) + err := UniCommitmentParamsEncoder(&buf, p, &scratch) + if err != nil { + panic(err) + } + + return uint64(buf.Len()) + } + return tlv.MakeDynamicRecord( + 0, p, size, UniCommitmentParamsEncoder, + UniCommitmentParamsDecoder, + ) +} + +// Sign creates a signed commitment over the UniCommitments. +func (p *UniCommitments) Sign(ctx context.Context, + signer lndclient.SignerClient, key keychain.KeyLocator, + mintPoint wire.OutPoint) error { + + digest := p.VerificationDigest(mintPoint) + sig, err := signer.SignMessage( + ctx, digest[:], key, lndclient.SignSchnorr(nil), + ) + if err != nil { + return err + } + + schnorrSig, err := lnwire.NewSigFromSchnorrRawSignature(sig) + if err != nil { + return err + } + + p.Sig.Val = schnorrSig + + return nil +} + +// Verify verifies the signature of the UniCommitments. +func (p *UniCommitments) Verify(mintPoint wire.OutPoint, + key *btcec.PublicKey) error { + + digest := p.VerificationDigest(mintPoint) + + sig, err := p.Sig.Val.ToSignature() + if err != nil { + return err + } + + if !sig.Verify(digest[:], key) { + return ErrSignatureVerificationFailed + } + + return nil +} diff --git a/proof/uni_commitments_test.go b/proof/uni_commitments_test.go new file mode 100644 index 000000000..157c36a2f --- /dev/null +++ b/proof/uni_commitments_test.go @@ -0,0 +1,146 @@ +package proof + +import ( + "bytes" + "context" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +type mockSigner struct { + lndclient.SignerClient + + privKey *btcec.PrivateKey +} + +func (r *mockSigner) SignMessage(_ context.Context, msg []byte, + _ keychain.KeyLocator, _ ...lndclient.SignMessageOption) ([]byte, + error) { + + sig, err := schnorr.Sign(r.privKey, msg) + if err != nil { + return nil, err + } + + return sig.Serialize(), nil +} + +// TestUniCommitmentParams tests encoding and decoding of the +// UniCommitmentParams struct. +func TestUniCommitmentParams(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params *UniCommitmentParams + }{ + { + name: "empty params", + params: &UniCommitmentParams{}, + }, + { + name: "params with values", + params: NewUniCommitmentParams(1, 31337), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Serialize the params and then deserialize them again. + var b bytes.Buffer + err := tc.params.Encode(&b) + require.NoError(t, err) + + deserializedParams := &UniCommitmentParams{} + err = deserializedParams.Decode(&b) + require.NoError(t, err) + + require.Equal(t, tc.params, deserializedParams) + }) + } +} + +// TestUniCommitments tests encoding and decoding of the UniCommitments struct. +func TestUniCommitments(t *testing.T) { + t.Parallel() + + randSig := func() lnwire.Sig { + sig, err := lnwire.NewSigFromSchnorrRawSignature( + test.RandBytes(64), + ) + require.NoError(t, err) + + return sig + } + + testCases := []struct { + name string + commitments *UniCommitments + }{ + { + name: "empty commitments", + commitments: &UniCommitments{}, + }, + { + name: "commitments with no params", + commitments: NewUniCommitments(nil, randSig()), + }, + { + name: "commitments with params", + commitments: NewUniCommitments(NewUniCommitmentParams( + 123, 768, + ), randSig()), + }, + } + + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + var ( + ctx = context.Background() + signer = &mockSigner{ + privKey: privKey, + } + loc keychain.KeyLocator + mintPoint = wire.OutPoint{ + Hash: test.RandHash(), + Index: 123, + } + ) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Serialize the commitments and then deserialize them + // again. + var b bytes.Buffer + err := tc.commitments.Encode(&b) + require.NoError(t, err) + + deserializedCommitments := &UniCommitments{} + err = deserializedCommitments.Decode(&b) + require.NoError(t, err) + + require.Equal( + t, tc.commitments, deserializedCommitments, + ) + + err = deserializedCommitments.Sign( + ctx, signer, loc, mintPoint, + ) + require.NoError(t, err) + + err = deserializedCommitments.Verify( + mintPoint, privKey.PubKey(), + ) + require.NoError(t, err) + }) + } +}