Skip to content

Commit d04ab96

Browse files
authored
Merge pull request #1677 from lightninglabs/price-oracle-metadata
price oracle: add intent, optional peer_id and metadata to `QueryAssetRates` RPC
2 parents 237dc3f + 3294fc1 commit d04ab96

32 files changed

+1757
-601
lines changed

config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ type Config struct {
211211

212212
PriceOracle rfq.PriceOracle
213213

214+
PriceOracleSendPeerID bool
215+
214216
UniverseStats universe.Telemetry
215217

216218
AuxLeafSigner *tapchannel.AuxLeafSigner

docs/release-notes/release-notes-0.7.0.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@
8181
tag and also needs to be toggled on via the `channel.noop-htlcs` configuration
8282
option.
8383

84+
- [Two new configuration values were added to improve privacy when using public
85+
or untrusted third-party price
86+
oracles](https://github.com/lightninglabs/taproot-assets/pull/1677):
87+
`experimental.rfq.sendpricehint` controls whether a price hint is queried
88+
from the local price oracle and sent to the peer when requesting a price
89+
quote (opt-in, default `false`). `experimental.rfq.priceoraclesendpeerid`
90+
controls whether the peer's identity public key is sent to the local price
91+
oracle when querying asset price rates.
92+
8493
## RPC Additions
8594

8695
- The [price oracle RPC calls now have an intent, optional peer ID and metadata

docs/rfq-and-decimal-display.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,42 @@ convert from mSAT to asset units as follows:
208208
`price_in_asset`
209209
* `Y` is the number of asset units per BTC, specified by `price_out_asset`
210210

211+
### Price oracle interaction
212+
213+
```mermaid
214+
sequenceDiagram
215+
actor User
216+
box Seller (user)
217+
participant NodeA as Node A
218+
participant OracleA as Price Oracle A
219+
end
220+
box Buyer (edge node)
221+
participant NodeB as Node B
222+
participant OracleB as Price Oracle B
223+
end
224+
225+
User->>+NodeA: SendPayment
226+
227+
NodeA->>+NodeA: AddAssetSellOrder
228+
229+
NodeA->>+OracleA: Get price rate hint<br/>(QueryAssetRate[type=SALE,intent=PAY_INVOICE_HINT,peer=NodeB])
230+
OracleA-->>-NodeA: Price rate hint
231+
232+
NodeA->>+NodeB: Send sell request with price<br/>rate hint over p2p
233+
234+
NodeB->>+OracleB: Determine actual price<br/>rate using suggested price<br/>(QueryAssetRate[type=PURCHASE,intent=PAY_INVOICE,peer=NodeA])
235+
OracleB-->>-NodeB: Actual price rate
236+
237+
NodeB-->>-NodeA: Return actual price rate
238+
239+
NodeA->>+OracleA: Validate actual price rate<br/>(QueryAssetRate[type=SALE,intent=PAY_INVOICE_QUALIFY,peer=NodeB])
240+
OracleA-->>-NodeA: Approve actual price rate
241+
242+
NodeA->>-NodeA: Send payment over LN using approved actual price rate
243+
244+
NodeA->>-User: Payment result
245+
```
246+
211247
## Buy Order (Receiving via an Invoice)
212248

213249
The buy order covers the second user story: The user wants to get paid, they
@@ -245,6 +281,43 @@ node as:
245281
* `M` is the number of mSAT in a BTC (100,000,000,000), specified by
246282
`price_in_asset`
247283

284+
### Price oracle interaction
285+
286+
```mermaid
287+
sequenceDiagram
288+
actor User
289+
box Buyer (user)
290+
participant NodeA as Node A
291+
participant OracleA as Price Oracle A
292+
end
293+
box Seller (edge node)
294+
participant NodeB as Node B
295+
participant OracleB as Price Oracle B
296+
end
297+
298+
User->>+NodeA: AddInvoice
299+
300+
NodeA->>+NodeA: AddAssetBuyOrder
301+
302+
NodeA->>+OracleA: Get price rate hint<br/>(QueryAssetRate[type=PURCHASE,intent=RECV_PAYMENT_HINT,peer=NodeB])
303+
OracleA-->>-NodeA: Price rate hint
304+
305+
NodeA->>+NodeB: Send buy request with price<br/>rate hint over p2p
306+
307+
NodeB->>+OracleB: Determine actual price<br/>rate using suggested price<br/>(QueryAssetRate[type=SALE,intent=RECV_PAYMENT,peer=NodeA])
308+
OracleB-->>-NodeB: Actual price rate
309+
310+
NodeB-->>-NodeA: Return actual price rate
311+
312+
NodeA->>+OracleA: Validate actual price rate<br/>(QueryAssetRate[type=PURCHASE,intent=RECV_PAYMENT_QUALIFY,peer=NodeB])
313+
OracleA-->>-NodeA: Approve actual price rate
314+
315+
NodeA->>-NodeA: Create invoice using actual price rate
316+
317+
NodeA->>-User: Invoice
318+
```
319+
320+
248321
## Examples
249322

250323
See `TestFindDecimalDisplayBoundaries` and `TestUsdToJpy` in

itest/oracle_harness.go

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/hex"
77
"fmt"
88
"net"
9+
"slices"
910
"testing"
1011
"time"
1112

@@ -16,6 +17,7 @@ import (
1617
"github.com/lightninglabs/taproot-assets/rpcutils"
1718
oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
1819
"github.com/lightningnetwork/lnd/cert"
20+
"github.com/stretchr/testify/mock"
1921
"github.com/stretchr/testify/require"
2022
"google.golang.org/grpc"
2123
"google.golang.org/grpc/credentials"
@@ -30,35 +32,43 @@ type oracleHarness struct {
3032
grpcListener net.Listener
3133
grpcServer *grpc.Server
3234

33-
// bidPrices is a map used internally by the oracle harness to store bid
35+
// Mock is a mock object that can optionally be used to track calls to
36+
// the oracle harness. If no call expectations are set, the prices from
37+
// the maps below will be used.
38+
// NOTE: When setting up the call expectations, we need to use the
39+
// actual fields of the `QueryAssetRatesRequest` message, since
40+
// otherwise it is much harder to match the calls nicely.
41+
mock.Mock
42+
43+
// buyPrices is a map used internally by the oracle harness to store buy
3444
// prices for certain assets. We use the asset specifier string as a
3545
// unique identifier, since it will either contain an asset ID or a
3646
// group key.
37-
bidPrices map[string]rfqmath.BigIntFixedPoint
47+
buyPrices map[string]rfqmath.BigIntFixedPoint
3848

39-
// askPrices is a map used internally by the oracle harness to store ask
40-
// prices for certain assets. We use the asset specifier string as a
41-
// unique identifier, since it will either contain an asset ID or a
49+
// sellPrices is a map used internally by the oracle harness to store
50+
// sell prices for certain assets. We use the asset specifier string as
51+
// a unique identifier, since it will either contain an asset ID or a
4252
// group key.
43-
askPrices map[string]rfqmath.BigIntFixedPoint
53+
sellPrices map[string]rfqmath.BigIntFixedPoint
4454
}
4555

4656
// newOracleHarness returns a new oracle harness instance that is set to listen
4757
// on the provided address.
4858
func newOracleHarness(listenAddr string) *oracleHarness {
4959
return &oracleHarness{
5060
listenAddr: listenAddr,
51-
bidPrices: make(map[string]rfqmath.BigIntFixedPoint),
52-
askPrices: make(map[string]rfqmath.BigIntFixedPoint),
61+
buyPrices: make(map[string]rfqmath.BigIntFixedPoint),
62+
sellPrices: make(map[string]rfqmath.BigIntFixedPoint),
5363
}
5464
}
5565

56-
// setPrice sets the target bid and ask price for the provided specifier.
57-
func (o *oracleHarness) setPrice(specifier asset.Specifier, bidPrice,
58-
askPrice rfqmath.BigIntFixedPoint) {
66+
// setPrice sets the target buy and sell price for the provided specifier.
67+
func (o *oracleHarness) setPrice(specifier asset.Specifier, buyPrice,
68+
sellPrice rfqmath.BigIntFixedPoint) {
5969

60-
o.bidPrices[specifier.String()] = bidPrice
61-
o.askPrices[specifier.String()] = askPrice
70+
o.buyPrices[specifier.String()] = buyPrice
71+
o.sellPrices[specifier.String()] = sellPrice
6272
}
6373

6474
// start runs the oracle harness.
@@ -113,14 +123,14 @@ func (o *oracleHarness) getAssetRates(specifier asset.Specifier,
113123
// Determine the rate based on the transaction type.
114124
var subjectAssetRate rfqmath.BigIntFixedPoint
115125
if transactionType == oraclerpc.TransactionType_PURCHASE {
116-
rate, ok := o.bidPrices[specifier.String()]
126+
rate, ok := o.buyPrices[specifier.String()]
117127
if !ok {
118128
return oraclerpc.AssetRates{}, fmt.Errorf("purchase "+
119129
"price not found for %s", specifier.String())
120130
}
121131
subjectAssetRate = rate
122132
} else {
123-
rate, ok := o.askPrices[specifier.String()]
133+
rate, ok := o.sellPrices[specifier.String()]
124134
if !ok {
125135
return oraclerpc.AssetRates{}, fmt.Errorf("sale "+
126136
"price not found for %s", specifier.String())
@@ -181,6 +191,18 @@ func (o *oracleHarness) QueryAssetRates(_ context.Context,
181191
req *oraclerpc.QueryAssetRatesRequest) (
182192
*oraclerpc.QueryAssetRatesResponse, error) {
183193

194+
// Return early with the mocked value if call expectations are set up.
195+
if hasExpectedCall(o.ExpectedCalls, "QueryAssetRates") {
196+
args := o.Called(
197+
req.TransactionType, req.SubjectAsset,
198+
req.SubjectAssetMaxAmount, req.PaymentAsset,
199+
req.PaymentAssetMaxAmount, req.AssetRatesHint,
200+
req.Intent, req.CounterpartyId, req.Metadata,
201+
)
202+
resp, _ := args.Get(0).(*oraclerpc.QueryAssetRatesResponse)
203+
return resp, args.Error(1)
204+
}
205+
184206
// Ensure that the payment asset is BTC. We only support BTC as the
185207
// payment asset in this example.
186208
if !rpcutils.IsAssetBtc(req.PaymentAsset) {
@@ -203,8 +225,8 @@ func (o *oracleHarness) QueryAssetRates(_ context.Context,
203225
return nil, fmt.Errorf("error parsing subject asset: %w", err)
204226
}
205227

206-
_, hasPurchase := o.bidPrices[specifier.String()]
207-
_, hasSale := o.askPrices[specifier.String()]
228+
_, hasPurchase := o.buyPrices[specifier.String()]
229+
_, hasSale := o.sellPrices[specifier.String()]
208230

209231
log.Infof("Have for %s, purchase=%v, sale=%v", specifier.String(),
210232
hasPurchase, hasSale)
@@ -324,3 +346,11 @@ func generateSelfSignedCert() (tls.Certificate, error) {
324346

325347
return tlsCert, nil
326348
}
349+
350+
// hasExpectedCall checks if the method call has been registered as an expected
351+
// call with the mock object.
352+
func hasExpectedCall(expectedCalls []*mock.Call, method string) bool {
353+
return slices.ContainsFunc(expectedCalls, func(call *mock.Call) bool {
354+
return call.Method == method
355+
})
356+
}

0 commit comments

Comments
 (0)