Skip to content

Commit 86adb91

Browse files
committed
itest: add oracle harness
Adds a simple oracle server harness, this is a copy of the oracle harness implementation in LitD. We need this to have greatest code coverage in our tapd itests and also to have moving prices during the itest execution, allowing the creation of more complex scenarios.
1 parent a78b7fa commit 86adb91

File tree

1 file changed

+325
-0
lines changed

1 file changed

+325
-0
lines changed

itest/oracle_harness.go

Lines changed: 325 additions & 0 deletions
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)