Skip to content
Merged
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
7 changes: 6 additions & 1 deletion docs/release-notes/release-notes-0.8.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@
`experimental.rfq.priceoracle*` allow disabling TLS, skipping
verification, or specifying custom certificates.

- [PR#1978](https://github.com/lightninglabs/taproot-assets/pull/1978)
also augments price oracle connections with support for macaroon
authentication. A macaroon path can be specified via the
`experimental.rfq.priceoraclemacaroonpath` config option.

## RPC Updates

- [PR#1766](https://github.com/lightninglabs/taproot-assets/pull/1766):
Expand Down Expand Up @@ -183,7 +188,7 @@
## Breaking Changes

- [PR#1935](https://github.com/lightninglabs/taproot-assets/pull/1935)
Renamed the RFQ configuration option `experimental.rfq.skipacceptquotepricecheck`
Renamed the RFQ configuration option `experimental.rfq.skipacceptquotepricecheck`
to `experimental.rfq.skipquoteacceptverify` for improved clarity.
Update your configuration files to use the new option name.

Expand Down
10 changes: 10 additions & 0 deletions rfq/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type CliConfig struct {

PriceOracleTLSCertPath string `long:"priceoracletlscertpath" description:"Path to a PEM-encoded x509 certificate to use when constructing a TLS connection with a price oracle."`

PriceOracleMacaroonPath string `long:"priceoraclemacaroonpath" description:"Path to the macaroon to use when connecting to the price oracle gRPC server."`

SendPriceHint bool `long:"sendpricehint" description:"Send a price hint from the local price oracle to the RFQ peer when requesting a quote. For privacy reasons, this should only be turned on for self-hosted or trusted price oracles."`

PriceOracleSendPeerId bool `long:"priceoraclesendpeerid" description:"Send the peer ID (public key of the peer) to the price oracle when requesting a price rate. For privacy reasons, this should only be turned on for self-hosted or trusted price oracles."`
Expand Down Expand Up @@ -107,6 +109,14 @@ func (c *CliConfig) Validate() error {
}
}

// A macaroon requires transport security. If a macaroon path is set
// but TLS is disabled, the gRPC dial will fail. Catch this early with
// a clear error.
if c.PriceOracleMacaroonPath != "" && c.PriceOracleTLSDisable {
return fmt.Errorf("priceoraclemacaroonpath requires " +
"price oracle TLS to be enabled")
}

// Ensure that if the portfolio pilot address is set, it is valid.
if c.PortfolioPilotAddress != "" {
_, err := ParsePortfolioPilotAddress(c.PortfolioPilotAddress)
Expand Down
34 changes: 34 additions & 0 deletions rfq/macaroon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package rfq

import (
"fmt"
"os"

"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"gopkg.in/macaroon.v2"
)

// NewMacaroonDialOption reads a macaroon file from disk and returns
// a gRPC DialOption that attaches it as per-RPC credentials.
func NewMacaroonDialOption(path string) (grpc.DialOption, error) {
macBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("unable to read macaroon "+
"path: %w", err)
}

mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode "+
"macaroon: %w", err)
}

cred, err := macaroons.NewMacaroonCredential(mac)
if err != nil {
return nil, fmt.Errorf("error creating macaroon "+
"credential: %w", err)
}

return grpc.WithPerRPCCredentials(cred), nil
}
98 changes: 98 additions & 0 deletions rfq/macaroon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package rfq

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
"gopkg.in/macaroon.v2"
)

// testCaseNewMacaroonDialOption is a test case for
// NewMacaroonDialOption.
type testCaseNewMacaroonDialOption struct {
name string
setup func(t *testing.T) string
expectError bool
}

// runNewMacaroonDialOptionTest runs a single test case.
func runNewMacaroonDialOptionTest(t *testing.T,
tc *testCaseNewMacaroonDialOption) {

t.Run(tc.name, func(t *testing.T) {
path := tc.setup(t)
opt, err := NewMacaroonDialOption(path)

if tc.expectError {
require.Error(t, err)
require.Nil(t, opt)
return
}

require.NoError(t, err)
require.NotNil(t, opt)
})
}

// TestNewMacaroonDialOption tests the NewMacaroonDialOption function.
func TestNewMacaroonDialOption(t *testing.T) {
testCases := []*testCaseNewMacaroonDialOption{
{
name: "valid macaroon",
setup: func(t *testing.T) string {
mac, err := macaroon.New(
[]byte("root-key"),
[]byte("id"),
"loc",
macaroon.LatestVersion,
)
require.NoError(t, err)

macBytes, err := mac.MarshalBinary()
require.NoError(t, err)

p := filepath.Join(
t.TempDir(), "test.macaroon",
)
err = os.WriteFile(
p, macBytes, 0600,
)
require.NoError(t, err)

return p
},
expectError: false,
},
{
name: "nonexistent path",
setup: func(t *testing.T) string {
return filepath.Join(
t.TempDir(), "missing.macaroon",
)
},
expectError: true,
},
{
name: "invalid file contents",
setup: func(t *testing.T) string {
p := filepath.Join(
t.TempDir(), "bad.macaroon",
)
err := os.WriteFile(
p, []byte("not a macaroon"),
0600,
)
require.NoError(t, err)

return p
},
expectError: true,
},
}

for _, tc := range testCases {
runNewMacaroonDialOptionTest(t, tc)
}
}
12 changes: 10 additions & 2 deletions rfq/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,10 @@ type RpcPriceOracle struct {
}

// NewRpcPriceOracle creates a new RPC price oracle handle given the address
// of the price oracle RPC server.
func NewRpcPriceOracle(addrStr string, tlsConfig *TLSConfig) (*RpcPriceOracle,
// of the price oracle RPC server. An optional macaroon dial option can be
// provided for authentication with the oracle server.
func NewRpcPriceOracle(addrStr string, tlsConfig *TLSConfig,
macaroonOpt fn.Option[grpc.DialOption]) (*RpcPriceOracle,
error) {

addr, err := ParsePriceOracleAddress(addrStr)
Expand Down Expand Up @@ -237,6 +239,12 @@ func NewRpcPriceOracle(addrStr string, tlsConfig *TLSConfig) (*RpcPriceOracle,
),
}

// If a macaroon dial option is provided, append it to the dial
// options so that the macaroon is sent with every RPC call.
macaroonOpt.WhenSome(func(opt grpc.DialOption) {
dialOpts = append(dialOpts, opt)
})

// Formulate the server address dial string.
serverAddr := fmt.Sprintf("%s:%s", addr.Hostname(), addr.Port())

Expand Down
10 changes: 8 additions & 2 deletions rfq/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ func runQuerySalePriceTest(t *testing.T, tc *testCaseQuerySalePrice) {
// Create a new RPC price oracle client and connect to the mock service.
serviceAddr := fmt.Sprintf("rfqrpc://%s", testServiceAddress)
insecureTLS := insecureTLS()
client, err := NewRpcPriceOracle(serviceAddr, insecureTLS)
client, err := NewRpcPriceOracle(
serviceAddr, insecureTLS,
fn.None[grpc.DialOption](),
)
require.NoError(t, err)

// Query for an ask price.
Expand Down Expand Up @@ -260,7 +263,10 @@ func runQueryPurchasePriceTest(t *testing.T, tc *testCaseQueryPurchasePrice) {

// Create a new RPC price oracle client and connect to the mock service.
serviceAddr := fmt.Sprintf("rfqrpc://%s", testServiceAddress)
client, err := NewRpcPriceOracle(serviceAddr, insecureTLS())
client, err := NewRpcPriceOracle(
serviceAddr, insecureTLS(),
fn.None[grpc.DialOption](),
)
require.NoError(t, err)

// Query for an ask price.
Expand Down
6 changes: 4 additions & 2 deletions sample-tapd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,7 @@
; Disable TLS for price oracle communication.
; experimental.rfq.priceoracletlsdisable=false

; Skip price oracle certificate verification, yielding an insecure (cleartext)
; channel with the price oracle. Should only be used for testing.
; Skip price oracle certificate verification. Should only be used for testing.
; experimental.rfq.priceoracletlsinsecure=false

; Disable use of the operating system's root CA list when verifying a
Expand All @@ -474,6 +473,9 @@
; secure communication with a price oracle.
; experimental.rfq.priceoracletlscertpath=

; Path to the macaroon to use when connecting to the price oracle gRPC server.
; experimental.rfq.priceoraclemacaroonpath=

; Send a price hint from the local price oracle to the RFQ peer when requesting
; a quote. For privacy reasons, this should only be turned on for self-hosted or
; trusted price oracles.
Expand Down
22 changes: 22 additions & 0 deletions tapcfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ type ExperimentalConfig struct {
func (c *ExperimentalConfig) CleanAndValidate() error {
c.Rfq.PriceOracleTLSCertPath = CleanAndExpandPath(
c.Rfq.PriceOracleTLSCertPath)
c.Rfq.PriceOracleMacaroonPath = CleanAndExpandPath(
c.Rfq.PriceOracleMacaroonPath)
return c.Rfq.Validate()
}

Expand Down Expand Up @@ -1264,6 +1266,26 @@ func getPriceOracleTLSConfig(rfqCfg rfq.CliConfig) (*rfq.TLSConfig, error) {
return tlsConfig, nil
}

// getPriceOracleMacaroonOpt reads the price oracle macaroon file
// from disk (if configured) and returns it as an optional gRPC dial
// option.
func getPriceOracleMacaroonOpt(
rfqCfg rfq.CliConfig) (fn.Option[grpc.DialOption], error) {

if rfqCfg.PriceOracleMacaroonPath == "" {
return fn.None[grpc.DialOption](), nil
}

opt, err := rfq.NewMacaroonDialOption(
rfqCfg.PriceOracleMacaroonPath,
)
if err != nil {
return fn.None[grpc.DialOption](), err
}

return fn.Some(opt), nil
}

// fileExists reports whether the named file or directory exists.
// This function is taken from https://github.com/btcsuite/btcd
func fileExists(name string) bool {
Expand Down
7 changes: 7 additions & 0 deletions tapcfg/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,15 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
"oracle configuration: %w", err)
}

macaroonOpt, err := getPriceOracleMacaroonOpt(rfqCfg)
if err != nil {
return nil, fmt.Errorf("unable to load price "+
"oracle macaroon: %w", err)
}

priceOracle, err = rfq.NewRpcPriceOracle(
rfqCfg.PriceOracleAddress, tlsConfig,
macaroonOpt,
)
if err != nil {
return nil, fmt.Errorf("unable to create price "+
Expand Down
Loading