Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/release-notes/release-notes-0.8.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
quotes are restored into the active map so payment flows survive
restarts.

* [PR#2051](https://github.com/lightninglabs/taproot-assets/pull/2051)
persists peer-accepted sell quotes to the database on acceptance and
restores them into the active cache on startup, ensuring sell-side
payment flows survive restarts.

* [PR#2010](https://github.com/lightninglabs/taproot-assets/pull/2010)
fixes an issue that prevented asset roots from being deleted on
universes with existing federation sync log entries.
Expand Down
22 changes: 17 additions & 5 deletions rfq/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const (
// quote that was persisted for historical SCID lookup.
//nolint:lll
RfqPolicyTypeAssetPeerAcceptedBuy RfqPolicyType = "RFQ_POLICY_TYPE_PEER_ACCEPTED_BUY"

// RfqPolicyTypeAssetPeerAcceptedSell identifies a peer-accepted
// sell quote that was persisted for restart continuity.
//nolint:lll
RfqPolicyTypeAssetPeerAcceptedSell RfqPolicyType = "RFQ_POLICY_TYPE_PEER_ACCEPTED_SELL"
)

// String converts the policy type to its string representation.
Expand All @@ -41,16 +46,23 @@ type PolicyStore interface {
StorePurchasePolicy(ctx context.Context, accept rfqmsg.SellAccept) error

// FetchAcceptedQuotes fetches all non-expired accepted quotes.
// Returns sale policies as buy accepts, purchase policies as sell
// accepts, and peer-accepted buy quotes separately.
// Returns sale policies as buy accepts, purchase policies as
// sell accepts, peer-accepted buy quotes, and peer-accepted
// sell quotes separately.
FetchAcceptedQuotes(ctx context.Context) ([]rfqmsg.BuyAccept,
[]rfqmsg.SellAccept, []rfqmsg.BuyAccept, error)
[]rfqmsg.SellAccept, []rfqmsg.BuyAccept,
[]rfqmsg.SellAccept, error)

// StorePeerAcceptedBuyQuote persists a peer-accepted buy quote for
// historical SCID-to-peer lookup.
// StorePeerAcceptedBuyQuote persists a peer-accepted buy quote
// for historical SCID-to-peer lookup.
StorePeerAcceptedBuyQuote(ctx context.Context,
accept rfqmsg.BuyAccept) error

// StorePeerAcceptedSellQuote persists a peer-accepted sell
// quote for restart continuity.
StorePeerAcceptedSellQuote(ctx context.Context,
accept rfqmsg.SellAccept) error

// LookUpScid looks up the peer associated with the given SCID from
// persisted peer-accepted buy quote policies.
LookUpScid(ctx context.Context, scid uint64) (route.Vertex, error)
Expand Down
12 changes: 12 additions & 0 deletions rfq/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,18 @@ func (m *Manager) handleIncomingMessage(ctx context.Context,
scid := msg.ShortChannelId()
m.orderHandler.peerSellQuotes.Store(scid, msg)

// Persist the peer sell quote to DB so that
// the peerSellQuotes cache survives restarts.
pStore := m.cfg.PolicyStore
storeErr := pStore.StorePeerAcceptedSellQuote(
ctx, msg,
)
if storeErr != nil {
log.Errorf("Failed to persist peer sell "+
"quote for SCID %d: %v", scid,
storeErr)
}

// Notify subscribers of the incoming peer accepted
// asset sell quote.
event := NewPeerAcceptedSellQuoteEvent(&msg)
Expand Down
17 changes: 15 additions & 2 deletions rfq/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ func (mockPolicyStore) StorePurchasePolicy(context.Context,
}

func (mockPolicyStore) FetchAcceptedQuotes(context.Context) (
[]rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept, error) {
[]rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept,
[]rfqmsg.SellAccept, error) {

return nil, nil, nil, nil
return nil, nil, nil, nil, nil
}

func (mockPolicyStore) StorePeerAcceptedBuyQuote(context.Context,
Expand All @@ -89,6 +90,12 @@ func (mockPolicyStore) StorePeerAcceptedBuyQuote(context.Context,
return nil
}

func (mockPolicyStore) StorePeerAcceptedSellQuote(context.Context,
rfqmsg.SellAccept) error {

return nil
}

func (mockPolicyStore) LookUpScid(_ context.Context,
_ uint64) (route.Vertex, error) {

Expand Down Expand Up @@ -349,6 +356,12 @@ func (f *failingPolicyStore) StorePeerAcceptedBuyQuote(context.Context,
return fmt.Errorf("simulated DB write failure")
}

func (f *failingPolicyStore) StorePeerAcceptedSellQuote(context.Context,
rfqmsg.SellAccept) error {

return fmt.Errorf("simulated DB write failure")
}

func (f *failingPolicyStore) LookUpScid(_ context.Context,
scid uint64) (route.Vertex, error) {

Expand Down
6 changes: 5 additions & 1 deletion rfq/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,7 @@ func (h *OrderHandler) RegisterAssetPurchasePolicy(ctx context.Context,

// restorePersistedPolicies restores persisted policies from the policy store.
func (h *OrderHandler) restorePersistedPolicies(ctx context.Context) error {
buyAccepts, sellAccepts, peerBuyAccepts,
buyAccepts, sellAccepts, peerBuyAccepts, peerSellAccepts,
err := h.cfg.PolicyStore.FetchAcceptedQuotes(ctx)
if err != nil {
return fmt.Errorf("error fetching persisted policies: %w", err)
Expand Down Expand Up @@ -1478,6 +1478,10 @@ func (h *OrderHandler) restorePersistedPolicies(ctx context.Context) error {
h.peerBuyQuotes.Store(accept.ShortChannelId(), accept)
}

for _, accept := range peerSellAccepts {
h.peerSellQuotes.Store(accept.ShortChannelId(), accept)
}

return nil
}

Expand Down
2 changes: 1 addition & 1 deletion tapdb/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (
// daemon.
//
// NOTE: This MUST be updated when a new migration is added.
LatestMigrationVersion = 54
LatestMigrationVersion = 55
)

// DatabaseBackend is an interface that contains all methods our different
Expand Down
67 changes: 58 additions & 9 deletions tapdb/rfq_policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,20 @@ func (s *PersistedPolicyStore) storePolicy(ctx context.Context,
})
}

// FetchAcceptedQuotes retrieves all non-expired policies from the database.
// Sale policies are returned as buy accepts, purchase policies as sell accepts,
// and peer-accepted buy quotes are returned separately.
// FetchAcceptedQuotes retrieves all non-expired policies from the
// database. Sale policies are returned as buy accepts, purchase
// policies as sell accepts, and peer-accepted buy/sell quotes are
// returned separately.
func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) (
[]rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept, error) {
[]rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept,
[]rfqmsg.SellAccept, error) {

readOpts := ReadTxOption()
var (
buyAccepts []rfqmsg.BuyAccept
sellAccepts []rfqmsg.SellAccept
peerBuyAccepts []rfqmsg.BuyAccept
buyAccepts []rfqmsg.BuyAccept
sellAccepts []rfqmsg.SellAccept
peerBuyAccepts []rfqmsg.BuyAccept
peerSellAccepts []rfqmsg.SellAccept
)
now := time.Now().UTC()

Expand Down Expand Up @@ -222,6 +225,17 @@ func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) (
peerBuyAccepts, accept,
)

case rfq.RfqPolicyTypeAssetPeerAcceptedSell:
accept, err := sellAcceptFromStored(policy)
if err != nil {
return fmt.Errorf("error restoring "+
"peer sell quote: %w",
err)
}
peerSellAccepts = append(
peerSellAccepts, accept,
)

default:
// This should never happen by assertion.
return fmt.Errorf("unknown policy type: %s",
Expand All @@ -232,10 +246,11 @@ func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) (
return nil
})
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}

return buyAccepts, sellAccepts, peerBuyAccepts, nil
return buyAccepts, sellAccepts, peerBuyAccepts,
peerSellAccepts, nil
}

// StorePeerAcceptedBuyQuote persists a peer-accepted buy quote for historical
Expand Down Expand Up @@ -267,6 +282,40 @@ func (s *PersistedPolicyStore) StorePeerAcceptedBuyQuote(ctx context.Context,
return s.storePolicy(ctx, record)
}

// StorePeerAcceptedSellQuote persists a peer-accepted sell quote for
// restart continuity.
func (s *PersistedPolicyStore) StorePeerAcceptedSellQuote(
ctx context.Context, acpt rfqmsg.SellAccept) error {

assetID, groupKey := specifierPointers(
acpt.Request.AssetSpecifier,
)
rateBytes := coefficientBytes(acpt.AssetRate.Rate)
expiry := acpt.AssetRate.Expiry.UTC()
paymentMax := int64(acpt.Request.PaymentMaxAmt)

record := rfqPolicy{
PolicyType: rfq.RfqPolicyTypeAssetPeerAcceptedSell,
Scid: uint64(acpt.ShortChannelId()),
RfqID: rfqIDArray(acpt.ID),
Peer: serializePeer(acpt.Peer),
AssetID: assetID,
AssetGroupKey: groupKey,
RateCoefficient: rateBytes,
RateScale: acpt.AssetRate.Rate.Scale,
ExpiryUnix: uint64(expiry.Unix()),
PaymentMaxMsat: fn.Ptr(paymentMax),
RequestPaymentMaxMsat: fn.Ptr(paymentMax),
PriceOracleMetadata: acpt.Request.PriceOracleMetadata,
RequestVersion: fn.Ptr(
uint32(acpt.Request.Version),
),
AgreedAt: acpt.AgreedAt.UTC(),
}

return s.storePolicy(ctx, record)
}

// LookUpScid looks up the peer associated with the given SCID by querying
// persisted peer-accepted buy quote policies.
func (s *PersistedPolicyStore) LookUpScid(ctx context.Context,
Expand Down
80 changes: 72 additions & 8 deletions tapdb/rfq_policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func TestFetchAcceptedQuotesSeparatesPeerAcceptedBuy(t *testing.T) {
err = store.StoreSalePolicy(ctx, saleAccept)
require.NoError(t, err)

buyAccepts, sellAccepts, peerBuys, err :=
buyAccepts, sellAccepts, peerBuys, peerSells, err :=
store.FetchAcceptedQuotes(ctx)
require.NoError(t, err)

Expand All @@ -151,6 +151,7 @@ func TestFetchAcceptedQuotesSeparatesPeerAcceptedBuy(t *testing.T) {
require.Len(t, buyAccepts, 1)
require.Len(t, sellAccepts, 0)
require.Len(t, peerBuys, 1)
require.Len(t, peerSells, 0)
require.Equal(t, saleAccept.ID, buyAccepts[0].ID)
require.Equal(t, accept.ID, peerBuys[0].ID)
}
Expand Down Expand Up @@ -216,11 +217,11 @@ func testSellAccept(t *testing.T) rfqmsg.SellAccept {
}
}

// TestFetchAcceptedQuotesAllThreeTypes verifies that FetchAcceptedQuotes
// correctly categorises all three policy types: sale policies appear as buy
// accepts, purchase policies appear as sell accepts, and peer-accepted buy
// quotes are returned separately.
func TestFetchAcceptedQuotesAllThreeTypes(t *testing.T) {
// TestFetchAcceptedQuotesAllFourTypes verifies that FetchAcceptedQuotes
// correctly categorises all four policy types: sale policies appear as
// buy accepts, purchase policies appear as sell accepts, and
// peer-accepted buy/sell quotes are returned separately.
func TestFetchAcceptedQuotesAllFourTypes(t *testing.T) {
t.Parallel()

ctx := context.Background()
Expand All @@ -239,17 +240,22 @@ func TestFetchAcceptedQuotesAllThreeTypes(t *testing.T) {
err = store.StorePeerAcceptedBuyQuote(ctx, peerBuy)
require.NoError(t, err)

buyAccepts, sellAccepts, peerBuys, err :=
peerSell := testSellAccept(t)
err = store.StorePeerAcceptedSellQuote(ctx, peerSell)
require.NoError(t, err)

buyAccepts, sellAccepts, peerBuys, peerSells, err :=
store.FetchAcceptedQuotes(ctx)
require.NoError(t, err)

// Sale → buyAccepts, Purchase → sellAccepts, PeerBuy → peerBuys.
require.Len(t, buyAccepts, 1)
require.Len(t, sellAccepts, 1)
require.Len(t, peerBuys, 1)
require.Len(t, peerSells, 1)
require.Equal(t, sale.ID, buyAccepts[0].ID)
require.Equal(t, purchase.ID, sellAccepts[0].ID)
require.Equal(t, peerBuy.ID, peerBuys[0].ID)
require.Equal(t, peerSell.ID, peerSells[0].ID)
}

// TestLookUpScidIgnoresSalePolicy verifies that a sale policy stored in the
Expand All @@ -269,3 +275,61 @@ func TestLookUpScidIgnoresSalePolicy(t *testing.T) {
require.Error(t, err)
require.Contains(t, err.Error(), "error fetching policy by SCID")
}

// TestStorePeerAcceptedSellQuote tests that a peer-accepted sell quote
// can be persisted and round-tripped through FetchAcceptedQuotes.
func TestStorePeerAcceptedSellQuote(t *testing.T) {
t.Parallel()

ctx := context.Background()
store := newPolicyStore(t)

accept := testSellAccept(t)

err := store.StorePeerAcceptedSellQuote(ctx, accept)
require.NoError(t, err)

_, _, _, peerSells, err := store.FetchAcceptedQuotes(ctx)
require.NoError(t, err)

require.Len(t, peerSells, 1)
require.Equal(t, accept.ID, peerSells[0].ID)
require.Equal(t, accept.Peer, peerSells[0].Peer)
require.Equal(
t, accept.Request.PaymentMaxAmt,
peerSells[0].Request.PaymentMaxAmt,
)
}

// TestFetchAcceptedQuotesSeparatesPeerAcceptedSell verifies that
// FetchAcceptedQuotes returns peer-accepted sell quotes in the fourth
// return value, separate from purchase policies.
func TestFetchAcceptedQuotesSeparatesPeerAcceptedSell(t *testing.T) {
t.Parallel()

ctx := context.Background()
store := newPolicyStore(t)

// Store a peer-accepted sell quote.
peerSell := testSellAccept(t)
err := store.StorePeerAcceptedSellQuote(ctx, peerSell)
require.NoError(t, err)

// Also store a regular purchase policy.
purchase := testSellAccept(t)
err = store.StorePurchasePolicy(ctx, purchase)
require.NoError(t, err)

buyAccepts, sellAccepts, peerBuys, peerSells, err :=
store.FetchAcceptedQuotes(ctx)
require.NoError(t, err)

// Purchase policy appears in sellAccepts, peer sell quote
// appears separately in peerSells.
require.Len(t, buyAccepts, 0)
require.Len(t, sellAccepts, 1)
require.Len(t, peerBuys, 0)
require.Len(t, peerSells, 1)
require.Equal(t, purchase.ID, sellAccepts[0].ID)
require.Equal(t, peerSell.ID, peerSells[0].ID)
}
Loading