diff --git a/address/book.go b/address/book.go index 95197a8eb..c19d5eb99 100644 --- a/address/book.go +++ b/address/book.go @@ -16,6 +16,7 @@ import ( "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" + lfn "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/keychain" ) @@ -105,6 +106,12 @@ type Storage interface { // (genesis + group key) associated with a given asset. QueryAssetGroup(context.Context, asset.ID) (*asset.AssetGroup, error) + // QueryAssetGroupByGroupKey fetches the asset group with a matching + // tweaked key, including the genesis information used to create the + // group. + QueryAssetGroupByGroupKey(context.Context, + *btcec.PublicKey) (*asset.AssetGroup, error) + // FetchAssetMetaByHash attempts to fetch an asset meta based on an // asset hash. FetchAssetMetaByHash(ctx context.Context, @@ -210,23 +217,53 @@ func NewBook(cfg BookConfig) *Book { // geneses already known to this node. If asset issuance was not previously // verified, we then query universes in our federation for issuance proofs. func (b *Book) QueryAssetInfo(ctx context.Context, - id asset.ID) (*asset.AssetGroup, error) { + specifier asset.Specifier) (asset.AssetGroup, error) { + + switch { + case specifier.HasGroupPubKey(): + return fn.MapOptionZ( + specifier.GroupKey(), + func(gk btcec.PublicKey) lfn.Result[asset.AssetGroup] { + return b.queryAssetInfoByGroupKey(ctx, gk) + }, + ).Unpack() + + case specifier.HasId(): + return fn.MapOptionZ( + specifier.ID(), + func(id asset.ID) lfn.Result[asset.AssetGroup] { + return b.queryAssetInfoByID(ctx, id) + }, + ).Unpack() + + default: + return asset.AssetGroup{}, fmt.Errorf("invalid specifier: %s", + &specifier) + } +} + +// queryAssetInfoByID attempts to locate asset genesis information by querying +// geneses by ID already known to this node. If asset issuance was not +// previously verified, we then query universes in our federation for issuance +// proofs. +func (b *Book) queryAssetInfoByID(ctx context.Context, + id asset.ID) lfn.Result[asset.AssetGroup] { // Check if we know of this asset ID already. assetGroup, err := b.cfg.Store.QueryAssetGroup(ctx, id) switch { case assetGroup != nil: - return assetGroup, nil + return lfn.Ok(*assetGroup) // Asset lookup failed gracefully; continue to asset lookup using the // AssetSyncer if enabled. case errors.Is(err, ErrAssetGroupUnknown): if b.cfg.Syncer == nil { - return nil, ErrAssetGroupUnknown + return lfn.Err[asset.AssetGroup](ErrAssetGroupUnknown) } case err != nil: - return nil, err + return lfn.Err[asset.AssetGroup](err) } log.Debugf("Asset %v is unknown, attempting to bootstrap", id.String()) @@ -234,14 +271,14 @@ func (b *Book) QueryAssetInfo(ctx context.Context, // Use the AssetSyncer to query our universe federation for the asset. err = b.cfg.Syncer.SyncAssetInfo(ctx, asset.NewSpecifierFromId(id)) if err != nil { - return nil, err + return lfn.Err[asset.AssetGroup](err) } // The asset genesis info may have been synced from a universe // server; query for the asset ID again. assetGroup, err = b.cfg.Store.QueryAssetGroup(ctx, id) if err != nil { - return nil, err + return lfn.Err[asset.AssetGroup](err) } log.Debugf("Bootstrap succeeded for asset %v", id.String()) @@ -257,11 +294,57 @@ func (b *Book) QueryAssetInfo(ctx context.Context, )) err = b.cfg.Syncer.EnableAssetSync(ctx, assetGroup) if err != nil { - return nil, err + return lfn.Err[asset.AssetGroup](err) + } + } + + return lfn.Ok(*assetGroup) +} + +// queryAssetInfoByID attempts to locate asset genesis information by querying +// geneses by group key already known to this node. If asset issuance was not +// previously verified, we then query universes in our federation for issuance +// proofs. +func (b *Book) queryAssetInfoByGroupKey(ctx context.Context, + gk btcec.PublicKey) lfn.Result[asset.AssetGroup] { + + // Check if we know of this group key already. + gkBytes := gk.SerializeCompressed() + assetGroup, err := b.cfg.Store.QueryAssetGroupByGroupKey(ctx, &gk) + switch { + case assetGroup != nil: + return lfn.Ok(*assetGroup) + + // Asset lookup failed gracefully; continue to asset lookup using the + // AssetSyncer if enabled. + case errors.Is(err, ErrAssetGroupUnknown): + if b.cfg.Syncer == nil { + return lfn.Err[asset.AssetGroup](ErrAssetGroupUnknown) } + + case err != nil: + return lfn.Err[asset.AssetGroup](err) } - return assetGroup, nil + log.Debugf("Asset group %x is unknown, attempting to bootstrap", + gkBytes) + + // Use the AssetSyncer to query our universe federation for the asset. + err = b.SyncAssetGroup(ctx, gk) + if err != nil { + return lfn.Err[asset.AssetGroup](err) + } + + // The asset genesis info may have been synced from a universe + // server; query for the group key again. + assetGroup, err = b.cfg.Store.QueryAssetGroupByGroupKey(ctx, &gk) + if err != nil { + return lfn.Err[asset.AssetGroup](err) + } + + log.Debugf("Bootstrap succeeded for group %x", gkBytes) + + return lfn.Ok(*assetGroup) } // SyncAssetGroup attempts to enable asset sync for the given asset group, then @@ -336,16 +419,16 @@ func (b *Book) FetchAssetMetaForAsset(ctx context.Context, // NewAddress creates a new Taproot Asset address based on the input parameters. func (b *Book) NewAddress(ctx context.Context, addrVersion Version, - assetID asset.ID, amount uint64, + specifier asset.Specifier, amount uint64, tapscriptSibling *commitment.TapscriptPreimage, proofCourierAddr url.URL, addrOpts ...NewAddrOpt) (*AddrWithKeyInfo, error) { // Before we proceed and make new keys, make sure that we actually know // of this asset ID, or can import it. - if _, err := b.QueryAssetInfo(ctx, assetID); err != nil { + if _, err := b.QueryAssetInfo(ctx, specifier); err != nil { return nil, fmt.Errorf("unable to make address for unknown "+ - "asset %x: %w", assetID[:], err) + "asset %s: %w", &specifier, err) } rawScriptKeyDesc, err := b.cfg.KeyRing.DeriveNextTaprootAssetKey(ctx) @@ -364,7 +447,7 @@ func (b *Book) NewAddress(ctx context.Context, addrVersion Version, } return b.NewAddressWithKeys( - ctx, addrVersion, assetID, amount, scriptKey, internalKeyDesc, + ctx, addrVersion, specifier, amount, scriptKey, internalKeyDesc, tapscriptSibling, proofCourierAddr, addrOpts..., ) } @@ -372,7 +455,7 @@ func (b *Book) NewAddress(ctx context.Context, addrVersion Version, // NewAddressWithKeys creates a new Taproot Asset address based on the input // parameters that include pre-derived script and internal keys. func (b *Book) NewAddressWithKeys(ctx context.Context, addrVersion Version, - assetID asset.ID, amount uint64, scriptKey asset.ScriptKey, + specifier asset.Specifier, amount uint64, scriptKey asset.ScriptKey, internalKeyDesc keychain.KeyDescriptor, tapscriptSibling *commitment.TapscriptPreimage, proofCourierAddr url.URL, addrOpts ...NewAddrOpt) (*AddrWithKeyInfo, @@ -381,7 +464,7 @@ func (b *Book) NewAddressWithKeys(ctx context.Context, addrVersion Version, // Before we proceed, we'll make sure that the asset group is known to // the local store. Otherwise, we can't make an address as we haven't // bootstrapped it. - assetGroup, err := b.QueryAssetInfo(ctx, assetID) + assetGroup, err := b.QueryAssetInfo(ctx, specifier) if err != nil { return nil, err } diff --git a/address/book_test.go b/address/book_test.go new file mode 100644 index 000000000..51e1d93e1 --- /dev/null +++ b/address/book_test.go @@ -0,0 +1,320 @@ +package address + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightningnetwork/lnd/keychain" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// TestBook_NewAddress tests that we can create a new address with the +// `NewAddress` method of the Book type, while mocking the necessary +// dependencies. +func TestBook_NewAddress(t *testing.T) { + mockStorage := &MockStorage{} + mockSyncer := &MockAssetSyncer{} + mockKeyRing := &MockKeyRing{} + + book := NewBook(BookConfig{ + Store: mockStorage, + Syncer: mockSyncer, + KeyRing: mockKeyRing, + Chain: TestNet3Tap, + StoreTimeout: time.Second, + }) + + ctx := context.Background() + assetID := asset.RandID(t) + specifier := asset.NewSpecifierFromId(assetID) + amount := uint64(100) + proofCourierAddr := url.URL{} + + mockKeyRing.On("DeriveNextTaprootAssetKey", ctx). + Return(keychain.KeyDescriptor{ + PubKey: test.RandPubKey(t), + }, nil). + Twice() + mockStorage.On("QueryAssetGroup", ctx, assetID). + Return(&asset.AssetGroup{ + Genesis: &asset.Genesis{ + Tag: "tag", + FirstPrevOut: test.RandOp(t), + }, + }, nil) + mockStorage.On("InsertInternalKey", ctx, mock.Anything). + Return(nil) + mockStorage.On("InsertScriptKey", ctx, mock.Anything, mock.Anything). + Return(nil) + mockStorage.On("InsertAddrs", ctx, mock.Anything). + Return(nil) + + addr, err := book.NewAddress( + ctx, V0, specifier, amount, nil, proofCourierAddr, + ) + require.NoError(t, err) + require.NotNil(t, addr) + + mockStorage.AssertExpectations(t) + mockKeyRing.AssertExpectations(t) + mockSyncer.AssertExpectations(t) +} + +// TestBook_QueryAssetInfo tests that we can query asset info, while mocking the +// necessary dependencies. +func TestBook_QueryAssetInfo(t *testing.T) { + ctx := context.Background() + assetID := asset.RandID(t) + groupKey, _ := btcec.NewPrivateKey() + groupPub := groupKey.PubKey() + groupKey2, _ := btcec.NewPrivateKey() + groupPub2 := groupKey2.PubKey() + + assetGroup := &asset.AssetGroup{ + Genesis: &asset.Genesis{ + Tag: "tag", + }, + GroupKey: &asset.GroupKey{ + GroupPubKey: *groupPub, + }, + } + + tests := []struct { + name string + specifier asset.Specifier + setupMocks func(*MockStorage, *MockAssetSyncer) + expectsError bool + }{{ + name: "asset found by ID", + specifier: asset.NewSpecifierFromId(assetID), + setupMocks: func(ms *MockStorage, msync *MockAssetSyncer) { + ms.On("QueryAssetGroup", ctx, assetID). + Return(assetGroup, nil). + Once() + }, + expectsError: false, + }, { + name: "asset found by group key", + specifier: asset.NewSpecifierFromGroupKey(*groupPub2), + setupMocks: func(ms *MockStorage, msync *MockAssetSyncer) { + ms.On("QueryAssetGroupByGroupKey", ctx, groupPub2). + Return(assetGroup, nil). + Once() + }, + expectsError: false, + }, { + name: "asset not found, syncer returns error", + specifier: asset.NewSpecifierFromId(assetID), + setupMocks: func(ms *MockStorage, msync *MockAssetSyncer) { + errUnknown := ErrAssetGroupUnknown + ms.On("QueryAssetGroup", ctx, assetID). + Return((*asset.AssetGroup)(nil), errUnknown). + Once() + msync.On("SyncAssetInfo", ctx, mock.Anything). + Return(errUnknown). + Once() + }, + expectsError: true, + }, { + name: "asset not found, syncer succeeds and asset found " + + "after sync", + specifier: asset.NewSpecifierFromId(assetID), + setupMocks: func(ms *MockStorage, msync *MockAssetSyncer) { + errUnknown := ErrAssetGroupUnknown + ms.On("QueryAssetGroup", ctx, assetID). + Return((*asset.AssetGroup)(nil), errUnknown). + Once() + msync.On("SyncAssetInfo", ctx, mock.Anything). + Return(nil). + Once() + ms.On("QueryAssetGroup", ctx, assetID). + Return(assetGroup, nil). + Once() + msync.On("EnableAssetSync", ctx, assetGroup). + Return(nil). + Once() + }, + expectsError: false, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mockStorage := &MockStorage{} + mockSyncer := &MockAssetSyncer{} + book := NewBook(BookConfig{ + Store: mockStorage, + Syncer: mockSyncer, + KeyRing: &MockKeyRing{}, + Chain: ChainParams{}, + StoreTimeout: time.Second, + }) + test.setupMocks(mockStorage, mockSyncer) + _, err := book.QueryAssetInfo(ctx, test.specifier) + if test.expectsError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + mockStorage.AssertExpectations(t) + mockSyncer.AssertExpectations(t) + }) + } +} + +// MockStorage is a mock implementation of the Storage interface. +type MockStorage struct { + mock.Mock +} + +func (m *MockStorage) GetOrCreateEvent(ctx context.Context, status Status, + addr *AddrWithKeyInfo, walletTx *lndclient.Transaction, + outputIdx uint32) (*Event, error) { + + args := m.Called(ctx, status, addr, walletTx, outputIdx) + return args.Get(0).(*Event), args.Error(1) +} + +func (m *MockStorage) QueryAddrEvents(ctx context.Context, + params EventQueryParams) ([]*Event, error) { + + args := m.Called(ctx, params) + return args.Get(0).([]*Event), args.Error(1) +} + +func (m *MockStorage) QueryEvent(ctx context.Context, addr *AddrWithKeyInfo, + outpoint wire.OutPoint) (*Event, error) { + + args := m.Called(ctx, addr, outpoint) + return args.Get(0).(*Event), args.Error(1) +} + +func (m *MockStorage) CompleteEvent(ctx context.Context, event *Event, + status Status, anchorPoint wire.OutPoint) error { + + args := m.Called(ctx, event, status, anchorPoint) + return args.Error(0) +} + +func (m *MockStorage) InsertAddrs(ctx context.Context, + addrs ...AddrWithKeyInfo) error { + + args := m.Called(ctx, addrs) + return args.Error(0) +} + +func (m *MockStorage) QueryAddrs(ctx context.Context, + params QueryParams) ([]AddrWithKeyInfo, error) { + + args := m.Called(ctx, params) + return args.Get(0).([]AddrWithKeyInfo), args.Error(1) +} + +func (m *MockStorage) QueryAssetGroup(ctx context.Context, + id asset.ID) (*asset.AssetGroup, error) { + + args := m.Called(ctx, id) + return args.Get(0).(*asset.AssetGroup), args.Error(1) +} + +func (m *MockStorage) QueryAssetGroupByGroupKey(ctx context.Context, + key *btcec.PublicKey) (*asset.AssetGroup, error) { + + args := m.Called(ctx, key) + return args.Get(0).(*asset.AssetGroup), args.Error(1) +} + +func (m *MockStorage) FetchAssetMetaByHash(ctx context.Context, + metaHash [asset.MetaHashLen]byte) (*proof.MetaReveal, error) { + + args := m.Called(ctx, metaHash) + return args.Get(0).(*proof.MetaReveal), args.Error(1) +} + +func (m *MockStorage) FetchAssetMetaForAsset(ctx context.Context, + assetID asset.ID) (*proof.MetaReveal, error) { + + args := m.Called(ctx, assetID) + return args.Get(0).(*proof.MetaReveal), args.Error(1) +} + +func (m *MockStorage) AddrByTaprootOutput(ctx context.Context, + key *btcec.PublicKey) (*AddrWithKeyInfo, error) { + + args := m.Called(ctx, key) + return args.Get(0).(*AddrWithKeyInfo), args.Error(1) +} + +func (m *MockStorage) SetAddrManaged(ctx context.Context, addr *AddrWithKeyInfo, + managedFrom time.Time) error { + + args := m.Called(ctx, addr, managedFrom) + return args.Error(0) +} + +func (m *MockStorage) InsertInternalKey(ctx context.Context, + keyDesc keychain.KeyDescriptor) error { + + args := m.Called(ctx, keyDesc) + return args.Error(0) +} + +func (m *MockStorage) InsertScriptKey(ctx context.Context, + scriptKey asset.ScriptKey, keyType asset.ScriptKeyType) error { + + args := m.Called(ctx, scriptKey, keyType) + return args.Error(0) +} + +// MockAssetSyncer is a mock implementation of the AssetSyncer interface. +type MockAssetSyncer struct { + mock.Mock +} + +func (m *MockAssetSyncer) SyncAssetInfo(ctx context.Context, + specifier asset.Specifier) error { + + args := m.Called(ctx, specifier) + return args.Error(0) +} + +func (m *MockAssetSyncer) EnableAssetSync(ctx context.Context, + groupInfo *asset.AssetGroup) error { + + args := m.Called(ctx, groupInfo) + return args.Error(0) +} + +// MockKeyRing is a mock implementation of the KeyRing interface. +type MockKeyRing struct { + mock.Mock +} + +func (m *MockKeyRing) DeriveNextTaprootAssetKey( + ctx context.Context) (keychain.KeyDescriptor, error) { + + args := m.Called(ctx) + return args.Get(0).(keychain.KeyDescriptor), args.Error(1) +} + +func (m *MockKeyRing) DeriveNextKey(ctx context.Context, + family keychain.KeyFamily) (keychain.KeyDescriptor, error) { + + args := m.Called(ctx, family) + return args.Get(0).(keychain.KeyDescriptor), args.Error(1) +} + +func (m *MockKeyRing) IsLocalKey(ctx context.Context, + desc keychain.KeyDescriptor) bool { + + args := m.Called(ctx, desc) + return args.Bool(0) +} diff --git a/asset/asset.go b/asset/asset.go index a4b415a36..5222d92be 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -466,6 +466,12 @@ func (s *Specifier) ID() fn.Option[ID] { return s.id } +// GroupKey returns the underlying asset group public key option of the +// specifier. +func (s *Specifier) GroupKey() fn.Option[btcec.PublicKey] { + return s.groupKey +} + // WhenGroupPubKey executes the given function if asset group public key field // is specified. func (s *Specifier) WhenGroupPubKey(f func(btcec.PublicKey)) { diff --git a/fn/encoding.go b/fn/encoding.go new file mode 100644 index 000000000..25f15311f --- /dev/null +++ b/fn/encoding.go @@ -0,0 +1,49 @@ +package fn + +import ( + "bytes" + "io" +) + +// Encoder is an interface that defines a method to encode data into an +// io.Writer. +type Encoder interface { + // Encode writes the encoded data to the provided io.Writer. + Encode(w io.Writer) error +} + +// Encode encodes the given Encoder into a byte slice. +func Encode(e Encoder) ([]byte, error) { + if e == nil { + return nil, nil + } + + var buf bytes.Buffer + err := e.Encode(&buf) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// Serializer is an interface that defines a method to serialize data +// into an io.Writer. +type Serializer interface { + Serialize(w io.Writer) error +} + +// Serialize encodes the given Serializer into a byte slice. +func Serialize(s Serializer) ([]byte, error) { + if s == nil { + return nil, nil + } + + var buf bytes.Buffer + err := s.Serialize(&buf) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/go.mod b/go.mod index 6b9446261..a640474b2 100644 --- a/go.mod +++ b/go.mod @@ -27,10 +27,10 @@ require ( github.com/lib/pq v1.10.9 github.com/lightninglabs/aperture v0.3.13-beta github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.3 - github.com/lightninglabs/lndclient v0.19.0-7 + github.com/lightninglabs/lndclient v0.19.0-9 github.com/lightninglabs/neutrino/cache v1.1.2 github.com/lightninglabs/taproot-assets/taprpc v1.0.7 - github.com/lightningnetwork/lnd v0.19.0-beta.rc5.0.20250611041824-9e9524766c8a + github.com/lightningnetwork/lnd v0.19.1-beta.rc1.0.20250623232057-b48e2763a798 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/fn/v2 v2.0.8 diff --git a/go.sum b/go.sum index 8ec061980..57f2dfd5e 100644 --- a/go.sum +++ b/go.sum @@ -1137,8 +1137,8 @@ github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.3 h1:NuDp6Z+QNM github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.3/go.mod h1:bDnEKRN1u13NFBuy/C+bFLhxA5bfd3clT25y76QY0AM= github.com/lightninglabs/lightning-node-connect/mailbox v1.0.1 h1:RWmohykp3n/DTMWY8b18RNTEcLDf+KT/AZHKYdOObkM= github.com/lightninglabs/lightning-node-connect/mailbox v1.0.1/go.mod h1:NYtNexZE9gO1eOeegTxmIW9fqanl7eZ9cOrE9yewSAk= -github.com/lightninglabs/lndclient v0.19.0-7 h1:8+wGQnO8KSUq9elzGLscBUGchID+bWvrpX2qCo+tU48= -github.com/lightninglabs/lndclient v0.19.0-7/go.mod h1:35d50tEMFxlJlKTZGYA6EdOllPsbxS4FUmEVbETUx+Q= +github.com/lightninglabs/lndclient v0.19.0-9 h1:ell27omDoks79upoAsO/7QY40O93ud4tAtBXXZilqok= +github.com/lightninglabs/lndclient v0.19.0-9/go.mod h1:35d50tEMFxlJlKTZGYA6EdOllPsbxS4FUmEVbETUx+Q= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2 h1:eFjp1dIB2BhhQp/THKrjLdlYuPugO9UU4kDqu91OX/Q= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= @@ -1149,8 +1149,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9 github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.19.0-beta.rc5.0.20250611041824-9e9524766c8a h1:V+YyjXA86E/dC8pa0mXKjs6yFdgpVvwvmIAmtbwIBnw= -github.com/lightningnetwork/lnd v0.19.0-beta.rc5.0.20250611041824-9e9524766c8a/go.mod h1:aMQspWfx+sBJdTA5zzvWp3ARnFbx3jtiAXcP6OjmeS8= +github.com/lightningnetwork/lnd v0.19.1-beta.rc1.0.20250623232057-b48e2763a798 h1:nSnOCqilf+ynsJlTOVOoNXEhjjxTrvcv6jxMyUlZJJE= +github.com/lightningnetwork/lnd v0.19.1-beta.rc1.0.20250623232057-b48e2763a798/go.mod h1:iHZ/FHFK00BqV6qgDkZZfqWE3LGtgE0U5KdO5WrM+eQ= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= diff --git a/internal/test/helpers.go b/internal/test/helpers.go index 654fc5ca8..2a775cbc0 100644 --- a/internal/test/helpers.go +++ b/internal/test/helpers.go @@ -18,6 +18,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc/signrpc" @@ -356,10 +357,10 @@ func HexTx(t testing.TB, tx *wire.MsgTx) string { return "" } - var buf bytes.Buffer - require.NoError(t, tx.Serialize(&buf)) + txBytes, err := fn.Serialize(tx) + require.NoError(t, err) - return hex.EncodeToString(buf.Bytes()) + return hex.EncodeToString(txBytes) } func ComputeTaprootScriptErr(witnessProgram []byte) ([]byte, error) { diff --git a/itest/addrs_test.go b/itest/addrs_test.go index a9c412090..5db568027 100644 --- a/itest/addrs_test.go +++ b/itest/addrs_test.go @@ -1083,8 +1083,7 @@ func fundPacket(t *harnessTest, tapd *tapdHarness, func maybeFundPacket(t *harnessTest, tapd *tapdHarness, vPkg *tappsbt.VPacket) (*wrpc.FundVirtualPsbtResponse, error) { - var buf bytes.Buffer - err := vPkg.Serialize(&buf) + psbtBytes, err := fn.Serialize(vPkg) require.NoError(t.t, err) ctxb := context.Background() @@ -1093,7 +1092,7 @@ func maybeFundPacket(t *harnessTest, tapd *tapdHarness, return tapd.FundVirtualPsbt(ctxt, &wrpc.FundVirtualPsbtRequest{ Template: &wrpc.FundVirtualPsbtRequest_Psbt{ - Psbt: buf.Bytes(), + Psbt: psbtBytes, }, }) } diff --git a/itest/assets_test.go b/itest/assets_test.go index b7fe5a917..d0448e610 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -544,10 +544,10 @@ func testMintAssetsWithTapscriptSibling(t *harnessTest) { TxOut: []*wire.TxOut{burnOutput}, } - var burnTxBuf bytes.Buffer - require.NoError(t.t, burnTx.Serialize(&burnTxBuf)) + burnTxBytes, err := fn.Serialize(&burnTx) + require.NoError(t.t, err) bobLnd.RPC.PublishTransaction(&walletrpc.Transaction{ - TxHex: burnTxBuf.Bytes(), + TxHex: burnTxBytes, }) // Bob should detect the TX, and the resulting confirmed UTXO once diff --git a/itest/mint_fund_seal_test.go b/itest/mint_fund_seal_test.go index 0aefaf354..96b890426 100644 --- a/itest/mint_fund_seal_test.go +++ b/itest/mint_fund_seal_test.go @@ -752,9 +752,9 @@ func signTransferWithTweakedScriptKey(t *harnessTest, ctxt context.Context, []*tappsbt.VPacket) { encodeVpsbt := func(psbt *tappsbt.VPacket) []byte { - var b bytes.Buffer - require.NoError(t.t, psbt.Serialize(&b)) - return b.Bytes() + packetBytes, err := fn.Serialize(psbt) + require.NoError(t.t, err) + return packetBytes } decodeVpsbt := func(psbt []byte) *tappsbt.VPacket { vpsbt, err := tappsbt.NewFromRawBytes( diff --git a/itest/multisig.go b/itest/multisig.go index 083845cfb..28aa64c72 100644 --- a/itest/multisig.go +++ b/itest/multisig.go @@ -402,12 +402,11 @@ func CommitVirtualPsbts(t *testing.T, funder commands.RpcClientsBundle, t.Logf("Funding packet: %v\n", spew.Sdump(packet)) - var buf bytes.Buffer - err := packet.Serialize(&buf) + packetBytes, err := fn.Serialize(packet) require.NoError(t, err) request := &wrpc.CommitVirtualPsbtsRequest{ - AnchorPsbt: buf.Bytes(), + AnchorPsbt: packetBytes, Fees: &wrpc.CommitVirtualPsbtsRequest_SatPerVbyte{ SatPerVbyte: uint64(feeRateSatPerKVByte / 1000), }, @@ -546,12 +545,11 @@ func PublishAndLogTransfer(t *testing.T, tapd commands.RpcClientsBundle, ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout) defer cancel() - var buf bytes.Buffer - err := btcPkt.Serialize(&buf) + btcPktBytes, err := fn.Serialize(btcPkt) require.NoError(t, err) request := &wrpc.PublishAndLogRequest{ - AnchorPsbt: buf.Bytes(), + AnchorPsbt: btcPktBytes, VirtualPsbts: make([][]byte, len(activeAssets)), PassiveAssetPsbts: make([][]byte, len(passiveAssets)), ChangeOutputIndex: commitResp.ChangeOutputIndex, @@ -819,13 +817,12 @@ func partialSignWithKey(t *testing.T, lnd *rpc.HarnessRPC, signInput.TaprootLeafScript = leafToSign signInput.SighashType = txscript.SigHashDefault - var buf bytes.Buffer - err := pkt.Serialize(&buf) + pktBytes, err := fn.Serialize(pkt) require.NoError(t, err) resp, err := lnd.WalletKit.SignPsbt( ctxt, &walletrpc.SignPsbtRequest{ - FundedPsbt: buf.Bytes(), + FundedPsbt: pktBytes, }, ) require.NoError(t, err) diff --git a/itest/psbt_test.go b/itest/psbt_test.go index 192b34da8..b93e69275 100644 --- a/itest/psbt_test.go +++ b/itest/psbt_test.go @@ -132,14 +132,13 @@ func testPsbtScriptHashLockSend(t *harnessTest) { splitCommitment.RootAsset = *senderOut.Copy() } - var b bytes.Buffer - err = fundedPacket.Serialize(&b) + fundedPktBytes, err := fn.Serialize(fundedPacket) require.NoError(t.t, err) // Now we'll attempt to complete the transfer. sendResp, err := bob.AnchorVirtualPsbts( ctxb, &wrpc.AnchorVirtualPsbtsRequest{ - VirtualPsbts: [][]byte{b.Bytes()}, + VirtualPsbts: [][]byte{fundedPktBytes}, }, ) require.NoError(t.t, err) @@ -256,13 +255,12 @@ func testPsbtScriptCheckSigSend(t *harnessTest) { fundedPacket.Inputs[0].TaprootBip32Derivation[0].LeafHashes = [][]byte{ leaf2Hash[:], } - var b bytes.Buffer - err = fundedPacket.Serialize(&b) + fundedPktBytes, err := fn.Serialize(fundedPacket) require.NoError(t.t, err) signedResp, err := bob.SignVirtualPsbt( ctxb, &wrpc.SignVirtualPsbtRequest{ - FundedPsbt: b.Bytes(), + FundedPsbt: fundedPktBytes, }, ) require.NoError(t.t, err) @@ -1109,13 +1107,13 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { btcPacket, err := tapsend.PrepareAnchoringTemplate(allPackets) require.NoError(t.t, err) - var btcPacketBuf bytes.Buffer - require.NoError(t.t, btcPacket.Serialize(&btcPacketBuf)) + btcPacketBytes, err := fn.Serialize(btcPacket) + require.NoError(t.t, err) commitReq := &wrpc.CommitVirtualPsbtsRequest{ VirtualPsbts: [][]byte{signedvPktBytes}, PassiveAssetPsbts: [][]byte{signPassiveResp.SignedPsbt}, - AnchorPsbt: btcPacketBuf.Bytes(), + AnchorPsbt: btcPacketBytes, Fees: &wrpc.CommitVirtualPsbtsRequest_SatPerVbyte{ SatPerVbyte: uint64(feeRateSatPerKVByte / 1000), }, @@ -1894,13 +1892,12 @@ func testPsbtSighashNone(t *harnessTest) { // has been generated. fundedPacket.Inputs[0].SighashType = txscript.SigHashNone - var b bytes.Buffer - err = fundedPacket.Serialize(&b) + fundedPktBytes, err := fn.Serialize(fundedPacket) require.NoError(t.t, err) signedResp, err := bob.SignVirtualPsbt( ctxb, &wrpc.SignVirtualPsbtRequest{ - FundedPsbt: b.Bytes(), + FundedPsbt: fundedPktBytes, }, ) require.NoError(t.t, err) @@ -1935,10 +1932,8 @@ func testPsbtSighashNone(t *harnessTest) { PrevWitnesses = witnessBackup // Serialize the edited signed packet. - var buffer bytes.Buffer - err = signedPacket.Serialize(&buffer) + signedBytes, err := fn.Serialize(signedPacket) require.NoError(t.t, err) - signedBytes := buffer.Bytes() // Now we'll attempt to complete the transfer. sendResp, err := bob.AnchorVirtualPsbts( @@ -2065,14 +2060,12 @@ func testPsbtSighashNoneInvalid(t *harnessTest) { // This is where we would normally set the sighash flag to SIGHASH_NONE, // but instead we skip that step to verify that the VM will invalidate // the transfer when any inputs or outputs are mutated. - - var b bytes.Buffer - err = fundedPacket.Serialize(&b) + fundedPktBytes, err := fn.Serialize(fundedPacket) require.NoError(t.t, err) signedResp, err := bob.SignVirtualPsbt( ctxb, &wrpc.SignVirtualPsbtRequest{ - FundedPsbt: b.Bytes(), + FundedPsbt: fundedPktBytes, }, ) require.NoError(t.t, err) @@ -2107,10 +2100,8 @@ func testPsbtSighashNoneInvalid(t *harnessTest) { PrevWitnesses = witnessBackup // Serialize the edited signed packet. - var buffer bytes.Buffer - err = signedPacket.Serialize(&buffer) + signedBytes, err := fn.Serialize(signedPacket) require.NoError(t.t, err) - signedBytes := buffer.Bytes() ctxc, streamCancel := context.WithCancel(ctxb) stream, err := bob.SubscribeSendEvents( @@ -2267,8 +2258,7 @@ func testPsbtTrustlessSwap(t *harnessTest) { []*psbt.TaprootBip32Derivation{trDerivation} btcPacket.Outputs[0].TaprootInternalKey = trDerivation.XOnlyPubKey - var b bytes.Buffer - err = btcPacket.Serialize(&b) + btcPacketBytes, err := fn.Serialize(btcPacket) require.NoError(t.t, err) // Now we need to commit the vPSBT and PSBT, creating all the related @@ -2276,7 +2266,7 @@ func testPsbtTrustlessSwap(t *harnessTest) { resp, err := alice.CommitVirtualPsbts( ctxb, &wrpc.CommitVirtualPsbtsRequest{ VirtualPsbts: [][]byte{signedResp.SignedPsbt}, - AnchorPsbt: b.Bytes(), + AnchorPsbt: btcPacketBytes, AnchorChangeOutput: &wrpc.CommitVirtualPsbtsRequest_Add{ Add: true, }, @@ -2318,14 +2308,13 @@ func testPsbtTrustlessSwap(t *harnessTest) { t.Logf("Alice BTC PSBT: %v", spew.Sdump(btcPacket)) - b.Reset() - err = btcPacket.Serialize(&b) + btcPacketBytes, err = fn.Serialize(btcPacket) require.NoError(t.t, err) // Now alice signs the bitcoin psbt. signPsbtResp := t.tapd.cfg.LndNode.RPC.SignPsbt( &walletrpc.SignPsbtRequest{ - FundedPsbt: b.Bytes(), + FundedPsbt: btcPacketBytes, }, ) @@ -2412,8 +2401,7 @@ func testPsbtTrustlessSwap(t *harnessTest) { // Now let's serialize the edited vPSBT and commit it to our bitcoin // PSBT. - b.Reset() - err = btcPacket.Serialize(&b) + btcPacketBytes, err = fn.Serialize(btcPacket) require.NoError(t.t, err) // This call will also fund the PSBT, which means that the bitcoin that @@ -2422,7 +2410,7 @@ func testPsbtTrustlessSwap(t *harnessTest) { resp, err = bob.CommitVirtualPsbts( ctxb, &wrpc.CommitVirtualPsbtsRequest{ VirtualPsbts: [][]byte{bobVPsbtBytes}, - AnchorPsbt: b.Bytes(), + AnchorPsbt: btcPacketBytes, AnchorChangeOutput: &wrpc.CommitVirtualPsbtsRequest_Add{ Add: true, }, @@ -3052,11 +3040,11 @@ func testPsbtLockTimeSend(t *harnessTest) { spendTx, err := psbt.Extract(btcPacket) require.NoError(t.t, err) - var spendTxBuf bytes.Buffer - require.NoError(t.t, spendTx.Serialize(&spendTxBuf)) + spendTxBytes, err := fn.Serialize(spendTx) + require.NoError(t.t, err) _, err = bobLnd.RPC.WalletKit.PublishTransaction( ctxt, &walletrpc.Transaction{ - TxHex: spendTxBuf.Bytes(), + TxHex: spendTxBytes, }, ) require.ErrorContains(t.t, err, "non final") @@ -3267,11 +3255,11 @@ func testPsbtRelativeLockTimeSend(t *harnessTest) { spendTx, err := psbt.Extract(btcPacket) require.NoError(t.t, err) - var spendTxBuf bytes.Buffer - require.NoError(t.t, spendTx.Serialize(&spendTxBuf)) + spendTxBytes, err := fn.Serialize(spendTx) + require.NoError(t.t, err) _, err = lndBob.RPC.WalletKit.PublishTransaction( ctxt, &walletrpc.Transaction{ - TxHex: spendTxBuf.Bytes(), + TxHex: spendTxBytes, }, ) require.ErrorContains(t.t, err, "non BIP68 final") @@ -3485,11 +3473,11 @@ func testPsbtRelativeLockTimeSendProofFail(t *harnessTest) { spendTxTimeLocked, err := psbt.Extract(btcPacketTimeLocked) require.NoError(t.t, err) - var spendTxBuf bytes.Buffer - require.NoError(t.t, spendTxTimeLocked.Serialize(&spendTxBuf)) + spendTxBytes, err := fn.Serialize(spendTxTimeLocked) + require.NoError(t.t, err) _, err = lndBob.RPC.WalletKit.PublishTransaction( ctxt, &walletrpc.Transaction{ - TxHex: spendTxBuf.Bytes(), + TxHex: spendTxBytes, }, ) require.ErrorContains(t.t, err, "non BIP68 final") @@ -3509,11 +3497,11 @@ func testPsbtRelativeLockTimeSendProofFail(t *harnessTest) { spendTxTimeLocked, err = psbt.Extract(btcPacket) require.NoError(t.t, err) - spendTxBuf.Reset() - require.NoError(t.t, spendTxTimeLocked.Serialize(&spendTxBuf)) + spendTxBytes, err = fn.Serialize(spendTxTimeLocked) + require.NoError(t.t, err) _, err = lndBob.RPC.WalletKit.PublishTransaction( ctxt, &walletrpc.Transaction{ - TxHex: spendTxBuf.Bytes(), + TxHex: spendTxBytes, }, ) require.NoError(t.t, err) @@ -3648,12 +3636,11 @@ func sendAssetAndAssert(ctx context.Context, t *harnessTest, alice, func signPacket(t *testing.T, lnd *node.HarnessNode, pkt *psbt.Packet) *psbt.Packet { - var buf bytes.Buffer - err := pkt.Serialize(&buf) + pktBytes, err := fn.Serialize(pkt) require.NoError(t, err) signResp := lnd.RPC.SignPsbt(&walletrpc.SignPsbtRequest{ - FundedPsbt: buf.Bytes(), + FundedPsbt: pktBytes, }) signedPacket, err := psbt.NewFromRawBytes( @@ -3667,12 +3654,11 @@ func signPacket(t *testing.T, lnd *node.HarnessNode, func finalizePacket(t *testing.T, lnd *node.HarnessNode, pkt *psbt.Packet) *psbt.Packet { - var buf bytes.Buffer - err := pkt.Serialize(&buf) + pktBytes, err := fn.Serialize(pkt) require.NoError(t, err) finalizeResp := lnd.RPC.FinalizePsbt(&walletrpc.FinalizePsbtRequest{ - FundedPsbt: buf.Bytes(), + FundedPsbt: pktBytes, }) signedPacket, err := psbt.NewFromRawBytes( diff --git a/itest/round_trip_send_test.go b/itest/round_trip_send_test.go index 34ad62ba2..53bad39dd 100644 --- a/itest/round_trip_send_test.go +++ b/itest/round_trip_send_test.go @@ -1,7 +1,6 @@ package itest import ( - "bytes" "context" "encoding/hex" @@ -10,6 +9,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/internal/test" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" @@ -183,11 +183,10 @@ func testRoundTripSend(t *harnessTest) { // We can now broadcast the transaction and wait for it to be mined. // Publish the sweep transaction and then mine it as well. - var buf bytes.Buffer - err = tx.Serialize(&buf) + txBytes, err := fn.Serialize(tx) require.NoError(t.t, err) t.tapd.cfg.LndNode.RPC.PublishTransaction(&walletrpc.Transaction{ - TxHex: buf.Bytes(), + TxHex: txBytes, }) // Mine one block which should contain the sweep transaction. diff --git a/key_ring.go b/key_ring.go index 0f4b5ef54..d277e59e7 100644 --- a/key_ring.go +++ b/key_ring.go @@ -2,8 +2,10 @@ package taprootassets import ( "context" + "crypto/sha256" "fmt" + "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/tapgarden" @@ -88,6 +90,23 @@ func (l *LndRpcKeyRing) IsLocalKey(ctx context.Context, return derived.PubKey.IsEqual(desc.PubKey) } +// DeriveSharedKey returns a shared secret key by performing +// Diffie-Hellman key derivation between the ephemeral public key and +// the key specified by the key locator (or the node's identity private +// key if no key locator is specified): +// +// P_shared = privKeyNode * ephemeralPubkey +// +// The resulting shared public key is serialized in the compressed +// format and hashed with SHA256, resulting in a final key length of 256 +// bits. +func (l *LndRpcKeyRing) DeriveSharedKey(ctx context.Context, + ephemeralPubKey *btcec.PublicKey, + keyLocator *keychain.KeyLocator) ([sha256.Size]byte, error) { + + return l.lnd.Signer.DeriveSharedKey(ctx, ephemeralPubKey, keyLocator) +} + // A compile time assertion to ensure LndRpcKeyRing meets the // tapgarden.KeyRing interface. var _ tapgarden.KeyRing = (*LndRpcKeyRing)(nil) diff --git a/psbt_channel_funder.go b/psbt_channel_funder.go index 7fc740e06..86d98307f 100644 --- a/psbt_channel_funder.go +++ b/psbt_channel_funder.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcutil/psbt" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/tapchannel" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" @@ -55,17 +56,17 @@ func (a *assetChanIntent) FundingPsbt() (*psbt.Packet, error) { func (a *assetChanIntent) BindPsbt(ctx context.Context, finalPSBT *psbt.Packet) error { - var psbtBuf bytes.Buffer - if err := finalPSBT.Serialize(&psbtBuf); err != nil { + psbtBytes, err := fn.Serialize(finalPSBT) + if err != nil { return fmt.Errorf("unable to serialize base PSBT: %w", err) } - _, err := a.lnd.Client.FundingStateStep( + _, err = a.lnd.Client.FundingStateStep( ctx, &lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: a.tempPID[:], - FundedPsbt: psbtBuf.Bytes(), + FundedPsbt: psbtBytes, SkipFinalize: true, }, }, diff --git a/rpcserver.go b/rpcserver.go index b62531342..95e714940 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1645,8 +1645,9 @@ func (r *rpcServer) NewAddr(ctx context.Context, // Now that we have all the params, we'll try to add a new // address to the addr book. addr, err = r.cfg.AddrBook.NewAddress( - ctx, addrVersion, assetID, req.Amt, tapscriptSibling, - *courierAddr, address.WithAssetVersion(assetVersion), + ctx, addrVersion, asset.NewSpecifierFromId(assetID), + req.Amt, tapscriptSibling, *courierAddr, + address.WithAssetVersion(assetVersion), ) if err != nil { return nil, fmt.Errorf("unable to make new addr: %w", @@ -1694,9 +1695,9 @@ func (r *rpcServer) NewAddr(ctx context.Context, // Now that we have all the params, we'll try to add a new // address to the addr book. addr, err = r.cfg.AddrBook.NewAddressWithKeys( - ctx, addrVersion, assetID, req.Amt, *scriptKey, - internalKey, tapscriptSibling, *courierAddr, - address.WithAssetVersion(assetVersion), + ctx, addrVersion, asset.NewSpecifierFromId(assetID), + req.Amt, *scriptKey, internalKey, tapscriptSibling, + *courierAddr, address.WithAssetVersion(assetVersion), ) if err != nil { return nil, fmt.Errorf("unable to make new addr: %w", @@ -2353,7 +2354,7 @@ func (r *rpcServer) FundVirtualPsbt(ctx context.Context, ChangeOutputIndex: 0, } for idx := range passivePackets { - response.PassiveAssetPsbts[idx], err = serialize( + response.PassiveAssetPsbts[idx], err = fn.Serialize( passivePackets[idx], ) if err != nil { @@ -2367,7 +2368,7 @@ func (r *rpcServer) FundVirtualPsbt(ctx context.Context, return nil, fmt.Errorf("only one packet supported") } - response.FundedPsbt, err = serialize(fundedVPkt.VPackets[0]) + response.FundedPsbt, err = fn.Serialize(fundedVPkt.VPackets[0]) if err != nil { return nil, fmt.Errorf("error serializing packet: %w", err) } @@ -2453,7 +2454,7 @@ func (r *rpcServer) SignVirtualPsbt(ctx context.Context, return nil, fmt.Errorf("error signing packet: %w", err) } - signedPsbtBytes, err := serialize(vPkt) + signedPsbtBytes, err := fn.Serialize(vPkt) if err != nil { return nil, fmt.Errorf("error serializing packet: %w", err) } @@ -2579,7 +2580,7 @@ func (r *rpcServer) CommitVirtualPsbts(ctx context.Context, // We're ready to attempt to fund the transaction now. For that we first // need to re-serialize our packet. - packetBytes, err := serialize(pkt) + packetBytes, err := fn.Serialize(pkt) if err != nil { return nil, fmt.Errorf("error serializing packet: %w", err) } @@ -2728,7 +2729,7 @@ func (r *rpcServer) CommitVirtualPsbts(ctx context.Context, ChangeOutputIndex: changeIndex, } - response.AnchorPsbt, err = serialize(fundedPacket) + response.AnchorPsbt, err = fn.Serialize(fundedPacket) if err != nil { return nil, fmt.Errorf("error serializing packet: %w", err) } @@ -3736,14 +3737,12 @@ func marshalOutboundParcel( // Serialize the anchor transaction if it exists. var anchorTxBytes []byte if parcel.AnchorTx != nil { - var b bytes.Buffer - err := parcel.AnchorTx.Serialize(&b) + var err error + anchorTxBytes, err = fn.Serialize(parcel.AnchorTx) if err != nil { return nil, fmt.Errorf("unable to serialize anchor "+ "tx: %w", err) } - - anchorTxBytes = b.Bytes() } return &taprpc.AssetTransfer{ @@ -4431,7 +4430,7 @@ func marshalMintingBatch(batch *tapgarden.MintingBatch, // If we have the genesis packet available (funded+signed), then we'll // display the txid as well. if batch.GenesisPacket != nil { - rpcBatch.BatchPsbt, err = serialize(batch.GenesisPacket.Pkt) + rpcBatch.BatchPsbt, err = fn.Serialize(batch.GenesisPacket.Pkt) if err != nil { return nil, fmt.Errorf("error serializing batch PSBT: "+ "%w", err) @@ -4587,16 +4586,13 @@ func marshalUnsealedSeedling(params chaincfg.Params, verbose bool, } // Serialize PSBT to bytes. - var psbtBuf bytes.Buffer - err = groupVirtualPacket.Serialize(&psbtBuf) + psbtBytes, err := fn.Serialize(groupVirtualPacket) if err != nil { return nil, fmt.Errorf("error serializing group "+ "virtual PSBT for unsealed seedling: %w", err) } - groupVirtualPsbt = base64.StdEncoding.EncodeToString( - psbtBuf.Bytes(), - ) + groupVirtualPsbt = base64.StdEncoding.EncodeToString(psbtBytes) } return &mintrpc.UnsealedAsset{ @@ -8531,18 +8527,6 @@ func (r *rpcServer) DeclareScriptKey(ctx context.Context, }, nil } -// serialize is a helper function that serializes a serializable object into a -// byte slice. -func serialize(s interface{ Serialize(io.Writer) error }) ([]byte, error) { - var b bytes.Buffer - err := s.Serialize(&b) - if err != nil { - return nil, err - } - - return b.Bytes(), nil -} - // decodeVirtualPackets decodes a slice of raw virtual packet bytes into a slice // of virtual packets. func decodeVirtualPackets(rawPackets [][]byte) ([]*tappsbt.VPacket, error) { @@ -8730,7 +8714,9 @@ func (r *rpcServer) DecodeAssetPayReq(ctx context.Context, // Next, we'll fetch the information for this asset ID through the addr // book. This'll automatically fetch the asset if needed. - assetGroup, err := r.cfg.AddrBook.QueryAssetInfo(ctx, assetID) + assetGroup, err := r.cfg.AddrBook.QueryAssetInfo( + ctx, asset.NewSpecifierFromId(assetID), + ) if err != nil { return nil, fmt.Errorf("unable to fetch asset info for "+ "asset_id=%x: %w", assetID[:], err) diff --git a/rpcutils/marshal.go b/rpcutils/marshal.go index 9393cb52c..b50d53ebc 100644 --- a/rpcutils/marshal.go +++ b/rpcutils/marshal.go @@ -505,8 +505,7 @@ func UnmarshalExternalKey(rpcKey *taprpc.ExternalKey) (asset.ExternalKey, func MarshalGroupVirtualTx(genTx *asset.GroupVirtualTx) (*taprpc.GroupVirtualTx, error) { - var groupTxBuf bytes.Buffer - err := genTx.Tx.Serialize(&groupTxBuf) + groupTxBytes, err := fn.Serialize(&genTx.Tx) if err != nil { return nil, err } @@ -517,7 +516,7 @@ func MarshalGroupVirtualTx(genTx *asset.GroupVirtualTx) (*taprpc.GroupVirtualTx, } return &taprpc.GroupVirtualTx{ - Transaction: groupTxBuf.Bytes(), + Transaction: groupTxBytes, PrevOut: &rpcPrevOut, GenesisId: fn.ByteSlice(genTx.GenID), TweakedKey: genTx.TweakedKey.SerializeCompressed(), @@ -575,13 +574,11 @@ func MarshalChainAsset(ctx context.Context, a asset.ChainAsset, var anchorTxBytes []byte if a.AnchorTx != nil { - var b bytes.Buffer - err := a.AnchorTx.Serialize(&b) + anchorTxBytes, err = fn.Serialize(a.AnchorTx) if err != nil { return nil, fmt.Errorf("unable to serialize anchor "+ "tx: %w", err) } - anchorTxBytes = b.Bytes() } rpcAsset.ChainAnchor = &taprpc.AnchorInfo{ diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index b66e20d11..7ff561531 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -192,7 +192,7 @@ type AssetSyncer interface { // not previously verified, we then query universes in our federation // for issuance proofs. QueryAssetInfo(ctx context.Context, - id asset.ID) (*asset.AssetGroup, error) + specifier asset.Specifier) (asset.AssetGroup, error) // FetchAssetMetaForAsset attempts to fetch an asset meta based on an // asset ID. @@ -1478,7 +1478,7 @@ func (f *FundingController) processFundingMsg(ctx context.Context, // Before we proceed, we'll make sure that we already know of // the genesis proof for the incoming asset. _, err = f.cfg.AssetSyncer.QueryAssetInfo( - ctx, assetProof.AssetID.Val, + ctx, asset.NewSpecifierFromId(assetProof.AssetID.Val), ) if err != nil { return tempPID, fmt.Errorf("unable to verify genesis "+ @@ -2051,7 +2051,7 @@ func (f *FundingController) fundingAssetGroupKey(ctx context.Context, var groupKey *btcec.PublicKey for _, a := range assetOutputs { info, err := f.cfg.AssetSyncer.QueryAssetInfo( - ctx, a.AssetID.Val, + ctx, asset.NewSpecifierFromId(a.AssetID.Val), ) switch { // If the asset isn't a grouped asset (or we don't know the diff --git a/tapdb/addrs.go b/tapdb/addrs.go index 21eab96ae..f43f13dfe 100644 --- a/tapdb/addrs.go +++ b/tapdb/addrs.go @@ -699,9 +699,9 @@ func (t *TapAddressBook) GetOrCreateEvent(ctx context.Context, writeTxOpts AddrBookTxOptions event *address.Event txHash = walletTx.Tx.TxHash() - txBuf bytes.Buffer ) - if err := walletTx.Tx.Serialize(&txBuf); err != nil { + txBytes, err := fn.Serialize(walletTx.Tx) + if err != nil { return nil, fmt.Errorf("error serializing tx: %w", err) } outpoint := wire.OutPoint{ @@ -727,7 +727,7 @@ func (t *TapAddressBook) GetOrCreateEvent(ctx context.Context, // transaction in our DB. txUpsert := ChainTxParams{ Txid: txHash[:], - RawTx: txBuf.Bytes(), + RawTx: txBytes, } if walletTx.Confirmations > 0 { txUpsert.BlockHeight = sqlInt32(walletTx.BlockHeight) @@ -1092,6 +1092,29 @@ func (t *TapAddressBook) QueryAssetGroup(ctx context.Context, return &assetGroup, nil } +// QueryAssetGroupByGroupKey fetches the asset group with a matching tweaked +// key, including the genesis information used to create the group. +func (t *TapAddressBook) QueryAssetGroupByGroupKey(ctx context.Context, + groupKey *btcec.PublicKey) (*asset.AssetGroup, error) { + + var ( + dbGroup *asset.AssetGroup + err error + ) + + readOpts := NewAssetStoreReadTx() + dbErr := t.db.ExecTx(ctx, &readOpts, func(a AddrBook) error { + dbGroup, err = fetchGroupByGroupKey(ctx, a, groupKey) + return err + }) + + if dbErr != nil { + return nil, dbErr + } + + return dbGroup, nil +} + // FetchAssetMetaByHash attempts to fetch an asset meta based on an asset hash. func (t *TapAddressBook) FetchAssetMetaByHash(ctx context.Context, metaHash [asset.MetaHashLen]byte) (*proof.MetaReveal, error) { diff --git a/tapdb/asset_minting.go b/tapdb/asset_minting.go index 4350acff4..f63b91ed3 100644 --- a/tapdb/asset_minting.go +++ b/tapdb/asset_minting.go @@ -343,8 +343,7 @@ func insertMintAnchorTx(ctx context.Context, q PendingAssetStore, return fmt.Errorf("%w: %w", ErrUpsertGenesisPoint, err) } - var psbtBuf bytes.Buffer - err = anchorPackage.Pkt.Serialize(&psbtBuf) + anchorPktBytes, err := fn.Serialize(anchorPackage.Pkt) if err != nil { return fmt.Errorf("%w: %w", ErrEncodePsbt, err) } @@ -354,7 +353,7 @@ func insertMintAnchorTx(ctx context.Context, q PendingAssetStore, _, err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ RawKey: rawBatchKey, - MintingTxPsbt: psbtBuf.Bytes(), + MintingTxPsbt: anchorPktBytes, ChangeOutputIndex: sqlInt32(anchorPackage.ChangeOutputIndex), AssetsOutputIndex: sqlInt32(anchorPackage.AssetAnchorOutIdx), GenesisID: sqlInt64(genesisPointDbID), @@ -1750,12 +1749,12 @@ func (a *AssetMintingStore) CommitSignedGenesisTx(ctx context.Context, // // TODO(roasbeef): lift all this above so don't need to encode, etc -- // also below? - var txBuf bytes.Buffer rawGenTx, err := psbt.Extract(genesisPkt.Pkt) if err != nil { return fmt.Errorf("unable to extract psbt packet: %w", err) } - if err := rawGenTx.Serialize(&txBuf); err != nil { + rawGenTxBytes, err := fn.Serialize(rawGenTx) + if err != nil { return err } @@ -1783,13 +1782,13 @@ func (a *AssetMintingStore) CommitSignedGenesisTx(ctx context.Context, return a.db.ExecTx(ctx, &writeTxOpts, func(q PendingAssetStore) error { // First, we'll update the genesis packet stored as part of the // batch, as this packet is now fully signed. - var psbtBuf bytes.Buffer - if err := genesisPkt.Pkt.Serialize(&psbtBuf); err != nil { + pktBytes, err := fn.Serialize(genesisPkt.Pkt) + if err != nil { return err } - err := q.UpdateBatchGenesisTx(ctx, GenesisTxUpdate{ + err = q.UpdateBatchGenesisTx(ctx, GenesisTxUpdate{ RawKey: rawBatchKey, - MintingTxPsbt: psbtBuf.Bytes(), + MintingTxPsbt: pktBytes, }) if err != nil { return fmt.Errorf("unable to update genesis tx: %w", @@ -1801,7 +1800,7 @@ func (a *AssetMintingStore) CommitSignedGenesisTx(ctx context.Context, // referenced by the managed UTXO. chainTXID, err := q.UpsertChainTx(ctx, ChainTxParams{ Txid: genTXID[:], - RawTx: txBuf.Bytes(), + RawTx: rawGenTxBytes, ChainFees: genesisPkt.ChainFees, }) if err != nil { diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index d44890991..02fdb4b04 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -1705,14 +1705,14 @@ func (a *AssetStore) importAssetFromProof(ctx context.Context, // // From the final asset snapshot, we'll obtain the final "resting // place" of the asset and insert that into the DB. - var anchorTxBuf bytes.Buffer - if err := proof.AnchorTx.Serialize(&anchorTxBuf); err != nil { + anchorTxBytes, err := fn.Serialize(proof.AnchorTx) + if err != nil { return err } anchorTXID := proof.AnchorTx.TxHash() chainTXID, err := db.UpsertChainTx(ctx, ChainTxParams{ Txid: anchorTXID[:], - RawTx: anchorTxBuf.Bytes(), + RawTx: anchorTxBytes, BlockHeight: sqlInt32(proof.AnchorBlockHeight), BlockHash: proof.AnchorBlockHash[:], TxIndex: sqlInt32(proof.AnchorTxIndex), @@ -2418,11 +2418,10 @@ func (a *AssetStore) LogPendingParcel(ctx context.Context, // Before we enter the DB transaction below, we'll use this space to // encode a few values outside the transaction closure. newAnchorTXID := spend.AnchorTx.TxHash() - var txBuf bytes.Buffer - if err := spend.AnchorTx.Serialize(&txBuf); err != nil { + anchorTxBytes, err := fn.Serialize(spend.AnchorTx) + if err != nil { return err } - anchorTxBytes := txBuf.Bytes() var writeTxOpts AssetStoreTxOptions return a.db.ExecTx(ctx, &writeTxOpts, func(q ActiveAssetsStore) error { diff --git a/tapdb/sqlutils_test.go b/tapdb/sqlutils_test.go index b74f731d1..5d8c5c7d5 100644 --- a/tapdb/sqlutils_test.go +++ b/tapdb/sqlutils_test.go @@ -144,13 +144,12 @@ func (d *DbHandler) AddRandomAssetProof(t *testing.T) (*asset.Asset, // We'll add the chain transaction of the proof now to simulate a // batched transfer on a higher layer. - var anchorTxBuf bytes.Buffer - err = annotatedProof.AnchorTx.Serialize(&anchorTxBuf) + anchorTxBytes, err := fn.Serialize(annotatedProof.AnchorTx) require.NoError(t, err) anchorTXID := annotatedProof.AnchorTx.TxHash() _, err = db.UpsertChainTx(ctx, ChainTxParams{ Txid: anchorTXID[:], - RawTx: anchorTxBuf.Bytes(), + RawTx: anchorTxBytes, BlockHeight: sqlInt32(annotatedProof.AnchorBlockHeight), BlockHash: annotatedProof.AnchorBlockHash[:], TxIndex: sqlInt32(annotatedProof.AnchorTxIndex), diff --git a/tapdb/universe.go b/tapdb/universe.go index d95ff989f..2a4e92c48 100644 --- a/tapdb/universe.go +++ b/tapdb/universe.go @@ -533,8 +533,8 @@ func upsertAssetGen(ctx context.Context, db UpsertAssetStore, } } - var txBuf bytes.Buffer - if err := genesisProof.AnchorTx.Serialize(&txBuf); err != nil { + txBytes, err := fn.Serialize(&genesisProof.AnchorTx) + if err != nil { return 0, fmt.Errorf("unable to serialize anchor tx: %w", err) } @@ -542,7 +542,7 @@ func upsertAssetGen(ctx context.Context, db UpsertAssetStore, genBlockHash := genesisProof.BlockHeader.BlockHash() chainTXID, err := db.UpsertChainTx(ctx, ChainTxParams{ Txid: genTXID[:], - RawTx: txBuf.Bytes(), + RawTx: txBytes, BlockHeight: sqlInt32(genesisProof.BlockHeight), BlockHash: genBlockHash[:], }) diff --git a/tapfreighter/coin_select.go b/tapfreighter/coin_select.go index 0a1c23de9..90b239328 100644 --- a/tapfreighter/coin_select.go +++ b/tapfreighter/coin_select.go @@ -85,7 +85,8 @@ func (s *CoinSelect) SelectCoins(ctx context.Context, }, ) if len(compatibleCommitments) == 0 { - return nil, ErrMatchingAssetsNotFound + return nil, fmt.Errorf("%w: no compatible commitments for max "+ + "version %v", ErrMatchingAssetsNotFound, maxVersion) } selectedCoins, err := s.selectForAmount( @@ -191,7 +192,9 @@ func (s *CoinSelect) selectForAmount(minTotalAmount uint64, // Having examined all the eligible commitments, return an error if the // minimal funding amount was not reached. if amountSum < minTotalAmount { - return nil, ErrMatchingAssetsNotFound + return nil, fmt.Errorf("%w: insufficient amount available, "+ + "have %d, want %d", ErrMatchingAssetsNotFound, + amountSum, minTotalAmount) } return selectedCommitments, nil } diff --git a/tapgarden/custodian_test.go b/tapgarden/custodian_test.go index 63859dedf..bdf0c9ba9 100644 --- a/tapgarden/custodian_test.go +++ b/tapgarden/custodian_test.go @@ -530,8 +530,8 @@ func TestCustodianNewAddr(t *testing.T) { proofCourierAddr := address.RandProofCourierAddr(t) addrVersion := test.RandFlip(address.V0, address.V1) dbAddr, err := h.addrBook.NewAddress( - ctx, addrVersion, addr.AssetID, addr.Amount, nil, - proofCourierAddr, + ctx, addrVersion, asset.NewSpecifierFromId(addr.AssetID), + addr.Amount, nil, proofCourierAddr, ) require.NoError(t, err) @@ -577,7 +577,8 @@ func TestBookAssetSyncer(t *testing.T) { newAsset := asset.RandAsset(t, asset.Type(test.RandInt31n(2))) addrVersion := test.RandFlip(address.V0, address.V1) _, err := h.addrBook.NewAddress( - ctx, addrVersion, newAsset.ID(), 1, nil, proofCourierAddr, + ctx, addrVersion, asset.NewSpecifierFromId(newAsset.ID()), 1, + nil, proofCourierAddr, ) require.ErrorContains(t, err, "unknown asset") @@ -597,7 +598,8 @@ func TestBookAssetSyncer(t *testing.T) { require.Eventually(t, func() bool { newAddr, err = h.addrBook.NewAddress( - ctx, addrVersion, newAsset.ID(), 1, nil, + ctx, addrVersion, + asset.NewSpecifierFromId(newAsset.ID()), 1, nil, proofCourierAddr, ) if err != nil { @@ -622,7 +624,8 @@ func TestBookAssetSyncer(t *testing.T) { secondAsset := asset.RandAsset(t, asset.Type(test.RandInt31n(2))) addrVersion = test.RandFlip(address.V0, address.V1) _, err = h.addrBook.NewAddress( - ctx, addrVersion, secondAsset.ID(), 1, nil, proofCourierAddr, + ctx, addrVersion, asset.NewSpecifierFromId(secondAsset.ID()), 1, + nil, proofCourierAddr, ) require.ErrorContains(t, err, "failed to fetch asset info") diff --git a/tapgarden/interface.go b/tapgarden/interface.go index 78e5199a0..bba7a60ba 100644 --- a/tapgarden/interface.go +++ b/tapgarden/interface.go @@ -2,6 +2,7 @@ package tapgarden import ( "context" + "crypto/sha256" "errors" "fmt" @@ -407,6 +408,19 @@ type KeyRing interface { // IsLocalKey returns true if the key is under the control of the wallet // and can be derived by it. IsLocalKey(context.Context, keychain.KeyDescriptor) bool + + // DeriveSharedKey returns a shared secret key by performing + // Diffie-Hellman key derivation between the ephemeral public key and + // the key specified by the key locator (or the node's identity private + // key if no key locator is specified): + // + // P_shared = privKeyNode * ephemeralPubkey + // + // The resulting shared public key is serialized in the compressed + // format and hashed with SHA256, resulting in a final key length of 256 + // bits. + DeriveSharedKey(context.Context, *btcec.PublicKey, + *keychain.KeyLocator) ([sha256.Size]byte, error) } var ( diff --git a/tapgarden/mock.go b/tapgarden/mock.go index 1326eec33..b0b9718f5 100644 --- a/tapgarden/mock.go +++ b/tapgarden/mock.go @@ -3,6 +3,7 @@ package tapgarden import ( "bytes" "context" + "crypto/sha256" "encoding/hex" "fmt" "math/rand" @@ -972,6 +973,28 @@ func (m *MockKeyRing) ScriptKeyAt(t *testing.T, idx uint32) asset.ScriptKey { }) } +func (m *MockKeyRing) DeriveSharedKey(_ context.Context, key *btcec.PublicKey, + locator *keychain.KeyLocator) ([sha256.Size]byte, error) { + + if locator == nil { + return [32]byte{}, fmt.Errorf("locator is nil") + } + + m.RLock() + defer m.RUnlock() + + priv, ok := m.Keys[*locator] + if !ok { + return [32]byte{}, fmt.Errorf("script key not found at index "+ + "%d", locator.Index) + } + + ecdh := &keychain.PrivKeyECDH{ + PrivKey: priv, + } + return ecdh.ECDH(key) +} + type MockGenSigner struct { KeyRing *MockKeyRing failSigning atomic.Bool diff --git a/tappsbt/decode_test.go b/tappsbt/decode_test.go index 8957b3ea0..7c2551f91 100644 --- a/tappsbt/decode_test.go +++ b/tappsbt/decode_test.go @@ -100,18 +100,14 @@ func TestGlobalUnknownFields(t *testing.T) { _, err = NewFromPsbt(packet) require.NoError(t, err) - var packetBuf bytes.Buffer - err = packet.Serialize(&packetBuf) + packetBytes, err := fn.Serialize(packet) require.NoError(t, err) - cloneBuffer := func(b *bytes.Buffer) *bytes.Buffer { - return bytes.NewBuffer(bytes.Clone(b.Bytes())) - } - // If we remove a mandatory VPacket field from the Packet, decoding // must fail. - invalidPacketBytes := cloneBuffer(&packetBuf) - invalidPacket, err := psbt.NewFromRawBytes(invalidPacketBytes, false) + invalidPacket, err := psbt.NewFromRawBytes( + bytes.NewReader(packetBytes), false, + ) require.NoError(t, err) invalidPacket.Unknowns = invalidPacket.Unknowns[1:] @@ -120,8 +116,9 @@ func TestGlobalUnknownFields(t *testing.T) { // If we add a global Unknown field to the valid Packet, decoding must // still succeed. - extraPacketBytes := cloneBuffer(&packetBuf) - extraPacket, err := psbt.NewFromRawBytes(extraPacketBytes, false) + extraPacket, err := psbt.NewFromRawBytes( + bytes.NewReader(packetBytes), false, + ) require.NoError(t, err) // The VPacket global Unknown keys start at 0x70, so we'll use a key diff --git a/tappsbt/encode.go b/tappsbt/encode.go index d64163f76..f5f23cc32 100644 --- a/tappsbt/encode.go +++ b/tappsbt/encode.go @@ -112,12 +112,12 @@ func (p *VPacket) Serialize(w io.Writer) error { // B64Encode returns the base64 encoding of the serialization of the current // virtual packet, or an error if the encoding fails. func (p *VPacket) B64Encode() (string, error) { - var b bytes.Buffer - if err := p.Serialize(&b); err != nil { + packetBytes, err := fn.Serialize(p) + if err != nil { return "", err } - return base64.StdEncoding.EncodeToString(b.Bytes()), nil + return base64.StdEncoding.EncodeToString(packetBytes), nil } // encode encodes the current VInput struct into a PInput and a wire.TxIn. diff --git a/tappsbt/interface.go b/tappsbt/interface.go index 0f5f38ebb..9c8ff17dd 100644 --- a/tappsbt/interface.go +++ b/tappsbt/interface.go @@ -934,13 +934,7 @@ func deserializeTweakedScriptKey(pOut psbt.POutput) (*asset.TweakedScriptKey, // Encode encodes the virtual packet into a byte slice. func Encode(vPkt *VPacket) ([]byte, error) { - var buf bytes.Buffer - err := vPkt.Serialize(&buf) - if err != nil { - return nil, err - } - - return buf.Bytes(), nil + return fn.Serialize(vPkt) } // Decode decodes a virtual packet from a byte slice. diff --git a/wallet_anchor.go b/wallet_anchor.go index 2669dd287..ed830d673 100644 --- a/wallet_anchor.go +++ b/wallet_anchor.go @@ -1,7 +1,6 @@ package taprootassets import ( - "bytes" "context" "fmt" "math" @@ -13,6 +12,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" "github.com/lightninglabs/taproot-assets/tapsend" @@ -47,8 +47,8 @@ func (l *LndRpcWalletAnchor) FundPsbt(ctx context.Context, packet *psbt.Packet, minConfs uint32, feeRate chainfee.SatPerKWeight, changeIdx int32) (*tapsend.FundedPsbt, error) { - var psbtBuf bytes.Buffer - if err := packet.Serialize(&psbtBuf); err != nil { + psbtBytes, err := fn.Serialize(packet) + if err != nil { return nil, fmt.Errorf("unable to encode psbt: %w", err) } @@ -57,7 +57,7 @@ func (l *LndRpcWalletAnchor) FundPsbt(ctx context.Context, packet *psbt.Packet, if changeIdx < 0 { fundTemplate = &walletrpc.FundPsbtRequest_CoinSelect{ CoinSelect: &walletrpc.PsbtCoinSelect{ - Psbt: psbtBuf.Bytes(), + Psbt: psbtBytes, ChangeOutput: &walletrpc.PsbtCoinSelect_Add{ Add: true, }, @@ -70,7 +70,7 @@ func (l *LndRpcWalletAnchor) FundPsbt(ctx context.Context, packet *psbt.Packet, fundTemplate = &walletrpc.FundPsbtRequest_CoinSelect{ CoinSelect: &walletrpc.PsbtCoinSelect{ - Psbt: psbtBuf.Bytes(), + Psbt: psbtBytes, ChangeOutput: change, }, }