Skip to content

Commit

Permalink
itest: add testOutboundRSMacaroonEnforcement itest
Browse files Browse the repository at this point in the history
testOutboundRSMacaroonEnforcement tests that a valid macaroon including
the `remotesigner` entity is required to connect to a watch-only node
that uses an outbound remote signer, while the watch-only node is in the
state (WalletState_ALLOW_REMOTE_SIGNER) where it waits for the signer to
connect.
  • Loading branch information
ViktorTigerstrom committed Sep 2, 2024
1 parent 0c8d455 commit 50239ae
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 4 deletions.
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,10 @@ var allTestCases = []*lntest.TestCase{
Name: "outbound remote signer",
TestFunc: testOutboundRemoteSigner,
},
{
Name: "outbound remote signer macaroon enforcement",
TestFunc: testOutboundRSMacaroonEnforcement,
},
{
Name: "taproot coop close",
TestFunc: testTaprootCoopClose,
Expand Down
104 changes: 103 additions & 1 deletion itest/lnd_remote_signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ package itest
import (
"fmt"
"testing"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -324,7 +328,7 @@ func testOutboundRemoteSigner(ht *lntest.HarnessTest) {
"--remotesigner.timeout=30s",
"--remotesigner.requesttimeout=30s",
}, commitArgs...),
password,
password, true,
)

// As the signer node will make an outbound connection to the
Expand Down Expand Up @@ -412,6 +416,104 @@ func testOutboundRemoteSigner(ht *lntest.HarnessTest) {
}
}

// testOutboundRSMacaroonEnforcement tests that a valid macaroon including
// the `remotesigner` entity is required to connect to a watch-only node that
// uses an outbound remote signer, while the watch-only node is in the state
// where it waits for the signer to connect.
func testOutboundRSMacaroonEnforcement(ht *lntest.HarnessTest) {
// Ensure that the watch-only node uses a configuration that requires an
// outbound remote signer during startup.
watchOnlyArgs := []string{
"--remotesigner.enable",
"--remotesigner.signertype=outbound",
"--remotesigner.timeout=15s",
"--remotesigner.requesttimeout=15s",
}

// Create the watch-only node. Note that we require authentication for
// the watch-only node, as we want to test that the macaroon enforcement
// works as expected.
watchOnly := ht.CreateNewNode("WatchOnly", watchOnlyArgs, nil, false)

startChan := make(chan error)

// Start the watch-only node in a goroutine as it requires a remote
// signer to connect before it can fully start.
go func() {
startChan <- watchOnly.Start(ht.Context())
}()

// Wait and ensure that the watch-only node reaches the state where
// it waits for the remote signer to connect, as this is the state where
// we want to test the macaroon enforcement.
err := wait.Predicate(func() bool {
if watchOnly.RPC == nil {
return false
}

state, err := watchOnly.RPC.State.GetState(
ht.Context(), &lnrpc.GetStateRequest{},
)
if err != nil {
return false
}

return state.State == lnrpc.WalletState_ALLOW_REMOTE_SIGNER
}, 5*time.Second)
require.NoError(ht, err)

// Set up a connection to the watch-only node. However, instead of using
// the watch-only node's admin macaroon, we'll use the invoice macaroon.
// The connection should not be allowed using this macaroon because it
// lacks the `remotesigner` entity required when the signer node
// connects to the watch-only node.
cfg := &lncfg.RemoteSigner{
SignerType: lncfg.SignerClientType,
RPCHost: watchOnly.Cfg.RPCAddr(),
MacaroonPath: watchOnly.Cfg.InvoiceMacPath,
TLSCertPath: watchOnly.Cfg.TLSCertPath,
Timeout: 10 * time.Second,
}
streamFeeder := rpcwallet.NewStreamFeeder(cfg)

stream, cleanup, err := streamFeeder.GetStream(ht.Context())
require.NoError(ht, err)

defer cleanup()

// Since we're using an unauthorized macaroon, we should expect to be
// denied access to the watch-only node.
_, err = stream.Recv()
require.ErrorContains(ht, err, "permission denied")

// Finally, connect a real signer to the watch-only node so that
// it can start up properly.
signerArgs := []string{
"--remotesigner.signertype=signer",
"--remotesigner.timeout=30s",
"--remotesigner.requesttimeout=10s",
fmt.Sprintf(
"--remotesigner.rpchost=localhost:%d",
watchOnly.Cfg.RPCPort,
),
fmt.Sprintf(
"--remotesigner.tlscertpath=%s",
watchOnly.Cfg.TLSCertPath,
),
fmt.Sprintf(
"--remotesigner.macaroonpath=%s",
watchOnly.Cfg.AdminMacPath, // An authorized macaroon.
),
}

_ = ht.NewNode("Signer", signerArgs)

// Finally, wait and ensure that the watch-only node is able to start
// up properly.
err = <-startChan
require.NoError(ht, err, "Shouldn't error on watch-only node startup")
}

// deriveCustomScopeAccounts derives the first 255 default accounts of the custom lnd
// internal key scope.
func deriveCustomScopeAccounts(t *testing.T) []*lnrpc.WatchOnlyAccount {
Expand Down
6 changes: 3 additions & 3 deletions lntest/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ func (h *HarnessTest) NewNodeWithSeedEtcd(name string, etcdCfg *etcd.Config,
func (h *HarnessTest) NewNodeWatchOnly(name string, extraArgs []string,
password []byte, watchOnly *lnrpc.WatchOnly) *node.HarnessNode {

hn := h.CreateNewNode(name, extraArgs, password)
hn := h.CreateNewNode(name, extraArgs, password, true)

h.StartWatchOnly(hn, name, password, watchOnly)

Expand All @@ -827,9 +827,9 @@ func (h *HarnessTest) NewNodeWatchOnly(name string, extraArgs []string,
// CreateNodeWatchOnly creates a new node and asserts its creation. The function
// will only create the node and will not start it.
func (h *HarnessTest) CreateNewNode(name string, extraArgs []string,
password []byte) *node.HarnessNode {
password []byte, noAuth bool) *node.HarnessNode {

hn, err := h.manager.newNode(h.T, name, extraArgs, password, true)
hn, err := h.manager.newNode(h.T, name, extraArgs, password, noAuth)
require.NoErrorf(h, err, "unable to create new node for %s", name)

return hn
Expand Down

0 comments on commit 50239ae

Please sign in to comment.