Skip to content

Commit 4852be4

Browse files
authored
Merge pull request #1395 from lightninglabs/itest-oracle-harness
Add itest oracle harness
2 parents af847c4 + 4048004 commit 4852be4

File tree

6 files changed

+528
-55
lines changed

6 files changed

+528
-55
lines changed

itest/log.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package itest
2+
3+
import (
4+
"github.com/btcsuite/btclog"
5+
"github.com/lightningnetwork/lnd/build"
6+
)
7+
8+
const Subsystem = "ITST"
9+
10+
// log is a logger that is initialized with no output filters. This means the
11+
// package will not perform any logging by default until the caller requests it.
12+
var log btclog.Logger
13+
14+
// The default amount of logging is none.
15+
func init() {
16+
UseLogger(build.NewSubLogger(Subsystem, nil))
17+
}
18+
19+
// UseLogger uses a specified Logger to output package logging info.
20+
// This should be used in preference to SetLogWriter if the caller is also
21+
// using btclog.
22+
func UseLogger(logger btclog.Logger) {
23+
log = logger
24+
}

itest/oracle_harness.go

+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
package itest
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"encoding/hex"
7+
"fmt"
8+
"net"
9+
"testing"
10+
"time"
11+
12+
"github.com/btcsuite/btcd/btcec/v2"
13+
"github.com/lightninglabs/taproot-assets/asset"
14+
"github.com/lightninglabs/taproot-assets/rfqmath"
15+
"github.com/lightninglabs/taproot-assets/rfqmsg"
16+
oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
17+
"github.com/lightningnetwork/lnd/cert"
18+
"github.com/stretchr/testify/require"
19+
"google.golang.org/grpc"
20+
"google.golang.org/grpc/credentials"
21+
)
22+
23+
// oracleHarness is a basic integration test RPC price oracle server harness.
24+
type oracleHarness struct {
25+
oraclerpc.UnimplementedPriceOracleServer
26+
27+
listenAddr string
28+
29+
grpcListener net.Listener
30+
grpcServer *grpc.Server
31+
32+
// bidPrices is a map used internally by the oracle harness to store bid
33+
// prices for certain assets. We use the asset specifier string as a
34+
// unique identifier, since it will either contain an asset ID or a
35+
// group key.
36+
bidPrices map[string]rfqmath.BigIntFixedPoint
37+
38+
// askPrices is a map used internally by the oracle harness to store ask
39+
// prices for certain assets. We use the asset specifier string as a
40+
// unique identifier, since it will either contain an asset ID or a
41+
// group key.
42+
askPrices map[string]rfqmath.BigIntFixedPoint
43+
}
44+
45+
// newOracleHarness returns a new oracle harness instance that is set to listen
46+
// on the provided address.
47+
func newOracleHarness(listenAddr string) *oracleHarness {
48+
return &oracleHarness{
49+
listenAddr: listenAddr,
50+
bidPrices: make(map[string]rfqmath.BigIntFixedPoint),
51+
askPrices: make(map[string]rfqmath.BigIntFixedPoint),
52+
}
53+
}
54+
55+
// setPrice sets the target bid and ask price for the provided specifier.
56+
func (o *oracleHarness) setPrice(specifier asset.Specifier, bidPrice,
57+
askPrice rfqmath.BigIntFixedPoint) {
58+
59+
o.bidPrices[specifier.String()] = bidPrice
60+
o.askPrices[specifier.String()] = askPrice
61+
}
62+
63+
// start runs the oracle harness.
64+
func (o *oracleHarness) start(t *testing.T) {
65+
// Start the mock RPC price oracle service.
66+
//
67+
// Generate self-signed certificate. This allows us to use TLS for the
68+
// gRPC server.
69+
tlsCert, err := generateSelfSignedCert()
70+
require.NoError(t, err)
71+
72+
// Create the gRPC server with TLS
73+
transportCredentials := credentials.NewTLS(&tls.Config{
74+
Certificates: []tls.Certificate{tlsCert},
75+
})
76+
o.grpcServer = grpc.NewServer(grpc.Creds(transportCredentials))
77+
78+
serviceAddr := fmt.Sprintf("rfqrpc://%s", o.listenAddr)
79+
log.Infof("Starting RPC price oracle service at address: %s\n",
80+
serviceAddr)
81+
82+
oraclerpc.RegisterPriceOracleServer(o.grpcServer, o)
83+
84+
go func() {
85+
var err error
86+
o.grpcListener, err = net.Listen("tcp", o.listenAddr)
87+
if err != nil {
88+
log.Errorf("Error oracle listening: %v", err)
89+
return
90+
}
91+
if err := o.grpcServer.Serve(o.grpcListener); err != nil {
92+
log.Errorf("Error oracle serving: %v", err)
93+
}
94+
}()
95+
}
96+
97+
// stop terminates the oracle harness.
98+
func (o *oracleHarness) stop() {
99+
if o.grpcServer != nil {
100+
o.grpcServer.Stop()
101+
}
102+
if o.grpcListener != nil {
103+
_ = o.grpcListener.Close()
104+
}
105+
}
106+
107+
// getAssetRates returns the asset rates for a given transaction type.
108+
func (o *oracleHarness) getAssetRates(specifier asset.Specifier,
109+
transactionType oraclerpc.TransactionType) (oraclerpc.AssetRates,
110+
error) {
111+
112+
// Determine the rate based on the transaction type.
113+
var subjectAssetRate rfqmath.BigIntFixedPoint
114+
if transactionType == oraclerpc.TransactionType_PURCHASE {
115+
rate, ok := o.bidPrices[specifier.String()]
116+
if !ok {
117+
return oraclerpc.AssetRates{}, fmt.Errorf("purchase "+
118+
"price not found for %s", specifier.String())
119+
}
120+
subjectAssetRate = rate
121+
} else {
122+
rate, ok := o.askPrices[specifier.String()]
123+
if !ok {
124+
return oraclerpc.AssetRates{}, fmt.Errorf("sale "+
125+
"price not found for %s", specifier.String())
126+
}
127+
subjectAssetRate = rate
128+
}
129+
130+
// Marshal subject asset rate to RPC format.
131+
rpcSubjectAssetToBtcRate, err := oraclerpc.MarshalBigIntFixedPoint(
132+
subjectAssetRate,
133+
)
134+
if err != nil {
135+
return oraclerpc.AssetRates{}, err
136+
}
137+
138+
// Marshal payment asset rate to RPC format.
139+
rpcPaymentAssetToBtcRate, err := oraclerpc.MarshalBigIntFixedPoint(
140+
rfqmsg.MilliSatPerBtc,
141+
)
142+
if err != nil {
143+
return oraclerpc.AssetRates{}, err
144+
}
145+
146+
expiry := time.Now().Add(5 * time.Minute).Unix()
147+
return oraclerpc.AssetRates{
148+
SubjectAssetRate: rpcSubjectAssetToBtcRate,
149+
PaymentAssetRate: rpcPaymentAssetToBtcRate,
150+
ExpiryTimestamp: uint64(expiry),
151+
}, nil
152+
}
153+
154+
// QueryAssetRates queries the asset rates for a given transaction type, subject
155+
// asset, and payment asset. An asset rate is the number of asset units per
156+
// BTC.
157+
//
158+
// Example use case:
159+
//
160+
// Alice is trying to pay an invoice by spending an asset. Alice therefore
161+
// requests that Bob (her asset channel counterparty) purchase the asset from
162+
// her. Bob's payment, in BTC, will pay the invoice.
163+
//
164+
// Alice requests a bid quote from Bob. Her request includes an asset rates hint
165+
// (ask). Alice obtains the asset rates hint by calling this endpoint. She sets:
166+
// - `SubjectAsset` to the asset she is trying to sell.
167+
// - `SubjectAssetMaxAmount` to the max channel asset outbound.
168+
// - `PaymentAsset` to BTC.
169+
// - `TransactionType` to SALE.
170+
// - `AssetRateHint` to nil.
171+
//
172+
// Bob calls this endpoint to get the bid quote asset rates that he will send as
173+
// a response to Alice's request. He sets:
174+
// - `SubjectAsset` to the asset that Alice is trying to sell.
175+
// - `SubjectAssetMaxAmount` to the value given in Alice's quote request.
176+
// - `PaymentAsset` to BTC.
177+
// - `TransactionType` to PURCHASE.
178+
// - `AssetRateHint` to the value given in Alice's quote request.
179+
func (o *oracleHarness) QueryAssetRates(_ context.Context,
180+
req *oraclerpc.QueryAssetRatesRequest) (
181+
*oraclerpc.QueryAssetRatesResponse, error) {
182+
183+
// Ensure that the payment asset is BTC. We only support BTC as the
184+
// payment asset in this example.
185+
if !oraclerpc.IsAssetBtc(req.PaymentAsset) {
186+
log.Infof("Payment asset is not BTC: %v", req.PaymentAsset)
187+
188+
return &oraclerpc.QueryAssetRatesResponse{
189+
Result: &oraclerpc.QueryAssetRatesResponse_Error{
190+
Error: &oraclerpc.QueryAssetRatesErrResponse{
191+
Message: "unsupported payment asset, " +
192+
"only BTC is supported",
193+
},
194+
},
195+
}, nil
196+
}
197+
198+
// Ensure that the subject asset is set correctly.
199+
specifier, err := parseSubjectAsset(req.SubjectAsset)
200+
if err != nil {
201+
log.Errorf("Error parsing subject asset: %v", err)
202+
return nil, fmt.Errorf("error parsing subject asset: %w", err)
203+
}
204+
205+
_, hasPurchase := o.bidPrices[specifier.String()]
206+
_, hasSale := o.askPrices[specifier.String()]
207+
208+
log.Infof("Have for %s, purchase=%v, sale=%v", specifier.String(),
209+
hasPurchase, hasSale)
210+
211+
// Ensure that the subject asset is supported.
212+
if !hasPurchase || !hasSale {
213+
log.Infof("Unsupported subject specifier: %v\n",
214+
req.SubjectAsset)
215+
216+
return &oraclerpc.QueryAssetRatesResponse{
217+
Result: &oraclerpc.QueryAssetRatesResponse_Error{
218+
Error: &oraclerpc.QueryAssetRatesErrResponse{
219+
Message: "unsupported subject asset",
220+
},
221+
},
222+
}, nil
223+
}
224+
225+
assetRates, err := o.getAssetRates(specifier, req.TransactionType)
226+
if err != nil {
227+
return nil, err
228+
}
229+
230+
log.Infof("QueryAssetRates returning rates (subject_asset_rate=%v, "+
231+
"payment_asset_rate=%v)", assetRates.SubjectAssetRate,
232+
assetRates.PaymentAssetRate)
233+
234+
return &oraclerpc.QueryAssetRatesResponse{
235+
Result: &oraclerpc.QueryAssetRatesResponse_Ok{
236+
Ok: &oraclerpc.QueryAssetRatesOkResponse{
237+
AssetRates: &assetRates,
238+
},
239+
},
240+
}, nil
241+
}
242+
243+
// parseSubjectAsset parses the subject asset from the given asset specifier.
244+
func parseSubjectAsset(subjectAsset *oraclerpc.AssetSpecifier) (asset.Specifier,
245+
error) {
246+
247+
// Ensure that the subject asset is set.
248+
if subjectAsset == nil {
249+
return asset.Specifier{}, fmt.Errorf("subject asset is not " +
250+
"set (nil)")
251+
}
252+
253+
// Check the subject asset bytes if set.
254+
var specifier asset.Specifier
255+
switch {
256+
case len(subjectAsset.GetAssetId()) > 0:
257+
var assetID asset.ID
258+
copy(assetID[:], subjectAsset.GetAssetId())
259+
specifier = asset.NewSpecifierFromId(assetID)
260+
261+
case len(subjectAsset.GetAssetIdStr()) > 0:
262+
assetIDBytes, err := hex.DecodeString(
263+
subjectAsset.GetAssetIdStr(),
264+
)
265+
if err != nil {
266+
return asset.Specifier{}, fmt.Errorf("error decoding "+
267+
"asset ID hex string: %w", err)
268+
}
269+
270+
var assetID asset.ID
271+
copy(assetID[:], assetIDBytes)
272+
specifier = asset.NewSpecifierFromId(assetID)
273+
274+
case len(subjectAsset.GetGroupKey()) > 0:
275+
groupKeyBytes := subjectAsset.GetGroupKey()
276+
groupKey, err := btcec.ParsePubKey(groupKeyBytes)
277+
if err != nil {
278+
return asset.Specifier{}, fmt.Errorf("error decoding "+
279+
"asset group key: %w", err)
280+
}
281+
282+
specifier = asset.NewSpecifierFromGroupKey(*groupKey)
283+
284+
case len(subjectAsset.GetGroupKeyStr()) > 0:
285+
groupKeyBytes, err := hex.DecodeString(
286+
subjectAsset.GetGroupKeyStr(),
287+
)
288+
if err != nil {
289+
return asset.Specifier{}, fmt.Errorf("error decoding "+
290+
" asset group key string: %w", err)
291+
}
292+
293+
groupKey, err := btcec.ParsePubKey(groupKeyBytes)
294+
if err != nil {
295+
return asset.Specifier{}, fmt.Errorf("error decoding "+
296+
"asset group key: %w", err)
297+
}
298+
299+
specifier = asset.NewSpecifierFromGroupKey(*groupKey)
300+
301+
default:
302+
return asset.Specifier{}, fmt.Errorf("subject asset " +
303+
"specifier is empty")
304+
}
305+
306+
return specifier, nil
307+
}
308+
309+
// generateSelfSignedCert generates a self-signed TLS certificate and private
310+
// key.
311+
func generateSelfSignedCert() (tls.Certificate, error) {
312+
certBytes, keyBytes, err := cert.GenCertPair(
313+
"itest price oracle", nil, nil, false, 24*time.Hour,
314+
)
315+
if err != nil {
316+
return tls.Certificate{}, err
317+
}
318+
319+
tlsCert, err := tls.X509KeyPair(certBytes, keyBytes)
320+
if err != nil {
321+
return tls.Certificate{}, err
322+
}
323+
324+
return tlsCert, nil
325+
}

0 commit comments

Comments
 (0)