Skip to content

Commit c05b20d

Browse files
committed
itest: add RPC oracle with spread based asset rates
This commit adds a fully configurable RPC oracle that can serve asset rates that differ for buy and sell. We then add a test that shows and asserts proper asset conversion to satoshis and back.
1 parent d5262c4 commit c05b20d

File tree

6 files changed

+552
-21
lines changed

6 files changed

+552
-21
lines changed

itest/assets_test.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -774,11 +774,13 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
774774
rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate)
775775
require.NoError(t, err)
776776

777+
t.Logf("Got quote for %v asset units per BTC", rate)
778+
777779
amountMsat := lnwire.MilliSatoshi(decodedInvoice.NumMsat)
778780
milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, *rate)
779781
numUnits := milliSatsFP.ScaleTo(0).ToUint64()
780-
msatPerUnit := uint64(decodedInvoice.NumMsat) / numUnits
781-
t.Logf("Got quote for %v asset units at %v msat/unit from peer %s "+
782+
msatPerUnit := float64(decodedInvoice.NumMsat) / float64(numUnits)
783+
t.Logf("Got quote for %v asset units at %3f msat/unit from peer %s "+
782784
"with SCID %d", numUnits, msatPerUnit, peerPubKey,
783785
acceptedQuote.Scid)
784786

@@ -825,15 +827,17 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode,
825827
rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate)
826828
require.NoError(t, err)
827829

830+
t.Logf("Got quote for %v asset units per BTC", rate)
831+
828832
assetUnits := rfqmath.NewBigIntFixedPoint(assetAmount, 0)
829833
numMSats := rfqmath.UnitsToMilliSatoshi(assetUnits, *rate)
830-
mSatPerUnit := uint64(decodedInvoice.NumMsat) / assetAmount
834+
mSatPerUnit := float64(decodedInvoice.NumMsat) / float64(assetAmount)
831835

832836
require.EqualValues(t, numMSats, decodedInvoice.NumMsat)
833837

834-
t.Logf("Got quote for %d sats at %v msat/unit from peer %x with SCID "+
835-
"%d", decodedInvoice.NumMsat, mSatPerUnit, dstRfqPeer.PubKey[:],
836-
resp.AcceptedBuyQuote.Scid)
838+
t.Logf("Got quote for %d mSats at %3f msat/unit from peer %x with "+
839+
"SCID %d", decodedInvoice.NumMsat, mSatPerUnit,
840+
dstRfqPeer.PubKey[:], resp.AcceptedBuyQuote.Scid)
837841

838842
return resp.InvoiceResult
839843
}

itest/litd_custom_channels_test.go

Lines changed: 230 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ package itest
33
import (
44
"context"
55
"fmt"
6+
"math"
7+
"math/big"
68
"slices"
79
"time"
810

911
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1012
"github.com/btcsuite/btcd/btcutil"
1113
"github.com/btcsuite/btcd/chaincfg/chainhash"
14+
"github.com/lightninglabs/taproot-assets/asset"
1215
"github.com/lightninglabs/taproot-assets/itest"
1316
"github.com/lightninglabs/taproot-assets/proof"
17+
"github.com/lightninglabs/taproot-assets/rfqmath"
1418
"github.com/lightninglabs/taproot-assets/taprpc"
1519
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
1620
tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
@@ -19,6 +23,7 @@ import (
1923
"github.com/lightningnetwork/lnd/fn"
2024
"github.com/lightningnetwork/lnd/lnrpc"
2125
"github.com/lightningnetwork/lnd/lntest"
26+
"github.com/lightningnetwork/lnd/lntest/port"
2227
"github.com/lightningnetwork/lnd/lntest/wait"
2328
"github.com/stretchr/testify/require"
2429
)
@@ -55,7 +60,7 @@ var (
5560
"--accept-keysend",
5661
"--debuglevel=trace,GRPC=error,BTCN=info",
5762
}
58-
litdArgsTemplate = []string{
63+
litdArgsTemplateNoOracle = []string{
5964
"--taproot-assets.allow-public-uni-proof-courier",
6065
"--taproot-assets.universe.public-access=rw",
6166
"--taproot-assets.universe.sync-all-assets",
@@ -64,17 +69,19 @@ var (
6469
"--taproot-assets.universerpccourier.numtries=5",
6570
"--taproot-assets.universerpccourier.initialbackoff=300ms",
6671
"--taproot-assets.universerpccourier.maxbackoff=600ms",
67-
"--taproot-assets.experimental.rfq.priceoracleaddress=" +
68-
"use_mock_price_oracle_service_promise_to_" +
69-
"not_use_on_mainnet",
70-
"--taproot-assets.experimental.rfq.mockoracleassetsperbtc=" +
71-
"5820600",
7272
"--taproot-assets.universerpccourier.skipinitdelay",
7373
"--taproot-assets.universerpccourier.backoffresetwait=100ms",
7474
"--taproot-assets.universerpccourier.initialbackoff=300ms",
7575
"--taproot-assets.universerpccourier.maxbackoff=600ms",
7676
"--taproot-assets.custodianproofretrievaldelay=500ms",
7777
}
78+
litdArgsTemplate = append(litdArgsTemplateNoOracle, []string{
79+
"--taproot-assets.experimental.rfq.priceoracleaddress=" +
80+
"use_mock_price_oracle_service_promise_to_" +
81+
"not_use_on_mainnet",
82+
"--taproot-assets.experimental.rfq.mockoracleassetsperbtc=" +
83+
"5820600",
84+
}...)
7885
)
7986

8087
const (
@@ -90,7 +97,7 @@ func testCustomChannelsLarge(_ context.Context, net *NetworkHarness,
9097
lndArgs := slices.Clone(lndArgsTemplate)
9198
litdArgs := slices.Clone(litdArgsTemplate)
9299

93-
// Explicitly set the proof courier as Alice (how has no other role
100+
// Explicitly set the proof courier as Zane (now has no other role
94101
// other than proof shuffling), otherwise a hashmail courier will be
95102
// used. For the funding transaction, we're just posting it and don't
96103
// expect a true receiver.
@@ -269,7 +276,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness,
269276
lndArgs := slices.Clone(lndArgsTemplate)
270277
litdArgs := slices.Clone(litdArgsTemplate)
271278

272-
// Explicitly set the proof courier as Alice (how has no other role
279+
// Explicitly set the proof courier as Zane (now has no other role
273280
// other than proof shuffling), otherwise a hashmail courier will be
274281
// used. For the funding transaction, we're just posting it and don't
275282
// expect a true receiver.
@@ -726,10 +733,10 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness,
726733
lndArgs := slices.Clone(lndArgsTemplate)
727734
litdArgs := slices.Clone(litdArgsTemplate)
728735

729-
// Explicitly set the proof courier as Alice (has no other role other
730-
// than proof shuffling), otherwise a hashmail courier will be used.
731-
// For the funding transaction, we're just posting it and don't expect a
732-
// true receiver.
736+
// Explicitly set the proof courier as Zane (now has no other role
737+
// other than proof shuffling), otherwise a hashmail courier will be
738+
// used. For the funding transaction, we're just posting it and don't
739+
// expect a true receiver.
733740
zane, err := net.NewNode(
734741
t.t, "Zane", lndArgs, false, true, litdArgs...,
735742
)
@@ -1118,7 +1125,10 @@ func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness,
11181125
lndArgs := slices.Clone(lndArgsTemplate)
11191126
litdArgs := slices.Clone(litdArgsTemplate)
11201127

1121-
// Zane will act as our Universe server for the duration of the test.
1128+
// Explicitly set the proof courier as Zane (now has no other role
1129+
// other than proof shuffling), otherwise a hashmail courier will be
1130+
// used. For the funding transaction, we're just posting it and don't
1131+
// expect a true receiver.
11221132
zane, err := net.NewNode(
11231133
t.t, "Zane", lndArgs, false, true, litdArgs...,
11241134
)
@@ -1479,7 +1489,10 @@ func testCustomChannelsBreach(_ context.Context, net *NetworkHarness,
14791489
lndArgs := slices.Clone(lndArgsTemplate)
14801490
litdArgs := slices.Clone(litdArgsTemplate)
14811491

1482-
// Zane will act as our Universe server for the duration of the test.
1492+
// Explicitly set the proof courier as Zane (now has no other role
1493+
// other than proof shuffling), otherwise a hashmail courier will be
1494+
// used. For the funding transaction, we're just posting it and don't
1495+
// expect a true receiver.
14831496
zane, err := net.NewNode(
14841497
t.t, "Zane", lndArgs, false, true, litdArgs...,
14851498
)
@@ -1700,7 +1713,7 @@ func testCustomChannelsLiquidityEdgeCases(_ context.Context,
17001713
lndArgs := slices.Clone(lndArgsTemplate)
17011714
litdArgs := slices.Clone(litdArgsTemplate)
17021715

1703-
// Explicitly set the proof courier as Alice (how has no other role
1716+
// Explicitly set the proof courier as Zane (now has no other role
17041717
// other than proof shuffling), otherwise a hashmail courier will be
17051718
// used. For the funding transaction, we're just posting it and don't
17061719
// expect a true receiver.
@@ -1957,6 +1970,10 @@ func testCustomChannelsBalanceConsistency(_ context.Context,
19571970
lndArgs := slices.Clone(lndArgsTemplate)
19581971
litdArgs := slices.Clone(litdArgsTemplate)
19591972

1973+
// Explicitly set the proof courier as Zane (now has no other role
1974+
// other than proof shuffling), otherwise a hashmail courier will be
1975+
// used. For the funding transaction, we're just posting it and don't
1976+
// expect a true receiver.
19601977
zane, err := net.NewNode(
19611978
t.t, "Zane", lndArgs, false, true, litdArgs...,
19621979
)
@@ -2147,3 +2164,201 @@ func testCustomChannelsBalanceConsistency(_ context.Context,
21472164
assertNumAssetOutputs(t.t, charlieTap, assetID, 1)
21482165
assertNumAssetOutputs(t.t, daveTap, assetID, 1)
21492166
}
2167+
2168+
// testCustomChannelsOraclePricing tests that all asset transfers are correctly
2169+
// priced when using an oracle that isn't tapd's mock oracle.
2170+
func testCustomChannelsOraclePricing(_ context.Context,
2171+
net *NetworkHarness, t *harnessTest) {
2172+
2173+
usdMetaData := &taprpc.AssetMeta{
2174+
Data: []byte(`{
2175+
"description":"this is a USD stablecoin with decimal display of 6"
2176+
}`),
2177+
Type: taprpc.AssetMetaType_META_TYPE_JSON,
2178+
}
2179+
2180+
const decimalDisplay = 6
2181+
itestAsset = &mintrpc.MintAsset{
2182+
AssetType: taprpc.AssetType_NORMAL,
2183+
Name: "USD",
2184+
AssetMeta: usdMetaData,
2185+
// We mint 1 million USD with a decimal display of 6, which
2186+
// results in 1 trillion asset units.
2187+
Amount: 1_000_000_000_000,
2188+
DecimalDisplay: decimalDisplay,
2189+
}
2190+
2191+
oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort())
2192+
oracle := newOracleHarness(oracleAddr)
2193+
oracle.start(t.t)
2194+
t.t.Cleanup(oracle.stop)
2195+
2196+
ctxb := context.Background()
2197+
lndArgs := slices.Clone(lndArgsTemplate)
2198+
litdArgs := slices.Clone(litdArgsTemplateNoOracle)
2199+
litdArgs = append(litdArgs, fmt.Sprintf(
2200+
"--taproot-assets.experimental.rfq.priceoracleaddress="+
2201+
"rfqrpc://%s", oracleAddr,
2202+
))
2203+
2204+
// Explicitly set the proof courier as Zane (now has no other role
2205+
// other than proof shuffling), otherwise a hashmail courier will be
2206+
// used. For the funding transaction, we're just posting it and don't
2207+
// expect a true receiver.
2208+
zane, err := net.NewNode(
2209+
t.t, "Zane", lndArgs, false, true, litdArgs...,
2210+
)
2211+
require.NoError(t.t, err)
2212+
2213+
litdArgs = append(litdArgs, fmt.Sprintf(
2214+
"--taproot-assets.proofcourieraddr=%s://%s",
2215+
proof.UniverseRpcCourierType, zane.Cfg.LitAddr(),
2216+
))
2217+
2218+
// The topology we are going for looks like the following:
2219+
//
2220+
// Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia
2221+
// |
2222+
// |
2223+
// [assets]
2224+
// |
2225+
// v
2226+
// Yara
2227+
//
2228+
// With [assets] being a custom channel and [sats] being a normal, BTC
2229+
// only channel.
2230+
// All 5 nodes need to be full litd nodes running in integrated mode
2231+
// with tapd included. We also need specific flags to be enabled, so we
2232+
// create 5 completely new nodes, ignoring the two default nodes that
2233+
// are created by the harness.
2234+
charlie, err := net.NewNode(
2235+
t.t, "Charlie", lndArgs, false, true, litdArgs...,
2236+
)
2237+
require.NoError(t.t, err)
2238+
2239+
dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...)
2240+
require.NoError(t.t, err)
2241+
erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...)
2242+
require.NoError(t.t, err)
2243+
fabia, err := net.NewNode(
2244+
t.t, "Fabia", lndArgs, false, true, litdArgs...,
2245+
)
2246+
require.NoError(t.t, err)
2247+
yara, err := net.NewNode(
2248+
t.t, "Yara", lndArgs, false, true, litdArgs...,
2249+
)
2250+
require.NoError(t.t, err)
2251+
2252+
nodes := []*HarnessNode{charlie, dave, erin, fabia, yara}
2253+
connectAllNodes(t.t, net, nodes)
2254+
fundAllNodes(t.t, net, nodes)
2255+
2256+
// Create the normal channel between Dave and Erin.
2257+
t.Logf("Opening normal channel between Dave and Erin...")
2258+
channelOp := openChannelAndAssert(
2259+
t, net, dave, erin, lntest.OpenChannelParams{
2260+
Amt: 10_000_000,
2261+
SatPerVByte: 5,
2262+
},
2263+
)
2264+
defer closeChannelAndAssert(t, net, dave, channelOp, false)
2265+
2266+
// This is the only public channel, we need everyone to be aware of it.
2267+
assertChannelKnown(t.t, charlie, channelOp)
2268+
assertChannelKnown(t.t, fabia, channelOp)
2269+
2270+
universeTap := newTapClient(t.t, zane)
2271+
charlieTap := newTapClient(t.t, charlie)
2272+
daveTap := newTapClient(t.t, dave)
2273+
erinTap := newTapClient(t.t, erin)
2274+
fabiaTap := newTapClient(t.t, fabia)
2275+
yaraTap := newTapClient(t.t, yara)
2276+
2277+
// Mint an asset on Charlie and sync Dave to Charlie as the universe.
2278+
mintedAssets := itest.MintAssetsConfirmBatch(
2279+
t.t, t.lndHarness.Miner.Client, charlieTap,
2280+
[]*mintrpc.MintAssetRequest{
2281+
{
2282+
Asset: itestAsset,
2283+
},
2284+
},
2285+
)
2286+
usdAsset := mintedAssets[0]
2287+
assetID := usdAsset.AssetGenesis.AssetId
2288+
2289+
// Now that we've minted the asset, we can set the price in the oracle.
2290+
var id asset.ID
2291+
copy(id[:], assetID)
2292+
2293+
// Let's assume the current USD price for 1 BTC is 66,548.40. We'll take
2294+
// that price and add a 4% spread, 2% on each side (buy/sell) to earn
2295+
// money as the oracle. 2% is 1,330.97, so we'll set the sell price to
2296+
// 65,217.43 and the purchase price to 67,879.37.
2297+
// The following numbers are to help understand the magic numbers below.
2298+
// They're the price in USD/BTC, the price of 1 USD in sats and the
2299+
// expected price in asset units per BTC.
2300+
// 65,217.43 => 1533.332 => 65_217_430_000
2301+
// 66,548.40 => 1502.666 => 66_548_400_000
2302+
// 67,879.37 => 1473.202 => 67_879_370_000
2303+
salePrice := rfqmath.NewBigIntFixedPoint(65_217_43, 2)
2304+
purchasePrice := rfqmath.NewBigIntFixedPoint(67_879_37, 2)
2305+
2306+
// We now have the prices defined in USD. But the asset has a decimal
2307+
// display of 6, so we need to multiply them by 10^6.
2308+
factor := rfqmath.NewBigInt(
2309+
big.NewInt(int64(math.Pow10(decimalDisplay))),
2310+
)
2311+
salePrice.Coefficient = salePrice.Coefficient.Mul(factor)
2312+
purchasePrice.Coefficient = purchasePrice.Coefficient.Mul(factor)
2313+
oracle.setPrice(id, purchasePrice, salePrice)
2314+
2315+
t.Logf("Minted %d USD assets, syncing universes...", usdAsset.Amount)
2316+
syncUniverses(t.t, charlieTap, dave, erin, fabia, yara)
2317+
t.Logf("Universes synced between all nodes, distributing assets...")
2318+
2319+
const (
2320+
daveFundingAmount = uint64(400_000_000)
2321+
erinFundingAmount = uint64(200_000_000)
2322+
)
2323+
charlieFundingAmount := usdAsset.Amount - uint64(2*400_000_000)
2324+
2325+
_, _, _ = createTestAssetNetwork(
2326+
t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap,
2327+
universeTap, usdAsset, daveFundingAmount, charlieFundingAmount,
2328+
daveFundingAmount, erinFundingAmount, 0,
2329+
)
2330+
2331+
// Before we start sending out payments, let's make sure each node can
2332+
// see the other one in the graph and has all required features.
2333+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave))
2334+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie))
2335+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara))
2336+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave))
2337+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia))
2338+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin))
2339+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin))
2340+
2341+
// We now create an invoice at Fabia for 100 USD, which is 100_000_000
2342+
// asset units with decimal display of 6.
2343+
const fabiaInvoiceAssetAmount = 100_000_000
2344+
invoiceResp := createAssetInvoice(
2345+
t.t, erin, fabia, fabiaInvoiceAssetAmount, assetID,
2346+
)
2347+
decodedInvoice, err := fabia.DecodePayReq(ctxb, &lnrpc.PayReqString{
2348+
PayReq: invoiceResp.PaymentRequest,
2349+
})
2350+
require.NoError(t.t, err)
2351+
2352+
// The invoice amount should come out as 100 * 1533.332.
2353+
require.EqualValues(t.t, 153_333_242, decodedInvoice.NumMsat)
2354+
2355+
numUnits := payInvoiceWithAssets(
2356+
t.t, charlie, dave, invoiceResp, assetID, false,
2357+
)
2358+
logBalance(t.t, nodes, assetID, "after invoice")
2359+
2360+
// The paid amount should come out as 153_333_242 / 1473.202, which is
2361+
// quite exactly 4% more than will arrive at the destination (which is
2362+
// the oracle's configured spread).
2363+
require.EqualValues(t.t, 104_081_638, numUnits)
2364+
}

itest/litd_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/btcsuite/btclog"
910
"github.com/lightningnetwork/lnd/build"
1011
"github.com/lightningnetwork/lnd/lntest"
1112
"github.com/lightningnetwork/lnd/signal"
@@ -131,6 +132,10 @@ func (h *harnessTest) setupLogging() {
131132
require.NoError(h.t, err)
132133
interceptor = &ic
133134

135+
UseLogger(build.NewSubLogger(Subsystem, func(tag string) btclog.Logger {
136+
return logWriter.GenSubLogger(tag, func() {})
137+
}))
138+
134139
err = build.ParseAndSetDebugLevels("debug", logWriter)
135140
require.NoError(h.t, err)
136141
}

0 commit comments

Comments
 (0)