From a6ea019f56d160c1dbeccfa1fc439f6ede38e17f Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 14 Oct 2021 15:43:00 +0200 Subject: [PATCH] lntest: add itest for remote signing --- lntest/harness.go | 33 +++ lntest/itest/lnd_remote_signer_test.go | 266 +++++++++++++++++++++++++ lntest/itest/lnd_test_list_on_test.go | 4 + 3 files changed, 303 insertions(+) create mode 100644 lntest/itest/lnd_remote_signer_test.go diff --git a/lntest/harness.go b/lntest/harness.go index 1c8386d6..3cba2a01 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -427,6 +427,39 @@ func (n *NetworkHarness) newNodeWithSeed(name string, extraArgs []string, return node, genSeedResp.CipherSeedMnemonic, response.AdminMacaroon, nil } +func (n *NetworkHarness) NewNodeRemoteSigner(name string, extraArgs []string, + password []byte, watchOnly *lnrpc.WatchOnly) (*HarnessNode, error) { + + node, err := n.newNode( + name, extraArgs, true, password, n.dbBackend, true, + ) + if err != nil { + return nil, err + } + + ctxb := context.Background() + + // With the seed created, construct the init request to the node, + // including the newly generated seed. + initReq := &lnrpc.InitWalletRequest{ + WalletPassword: password, + WatchOnly: watchOnly, + } + + // Pass the init request via rpc to finish unlocking the node. This will + // also initialize the macaroon-authenticated LightningClient. + _, err = node.Init(ctxb, initReq) + if err != nil { + return nil, err + } + + // With the node started, we can now record its public key within the + // global mapping. + n.RegisterNode(node) + + return node, nil +} + // RestoreNodeWithSeed fully initializes a HarnessNode using a chosen mnemonic, // password, recovery window, and optionally a set of static channel backups. // After providing the initialization request to unlock the node, this method diff --git a/lntest/itest/lnd_remote_signer_test.go b/lntest/itest/lnd_remote_signer_test.go new file mode 100644 index 00000000..b1bae04c --- /dev/null +++ b/lntest/itest/lnd_remote_signer_test.go @@ -0,0 +1,266 @@ +package itest + +import ( + "context" + "fmt" + "testing" + + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/stretchr/testify/require" +) + +var ( + rootKey = "tprv8ZgxMBicQKsPe6jS4vDm2n7s42Q6MpvghUQqMmSKG7bTZvGKtjrcU3" + + "PGzMNG37yzxywrcdvgkwrr8eYXJmbwdvUNVT4Ucv7ris4jvA7BUmg" + + nodePubKey = "033f55d436d4f7d24aeffb1b976647380f22ebf9e74390e8c76dcff" + + "9fea0093b7a" + + accounts = []*lnrpc.WatchOnlyAccount{{ + Purpose: waddrmgr.KeyScopeBIP0049Plus.Purpose, + // We always use the mainnet coin type for our BIP49/84 + // addresses! + CoinType: 0, + Account: 0, + Xpub: "tpubDDXEYWvGCTytEF6hBog9p4qr2QBUvJhh4P2wM4qHHv9N489khk" + + "QoGkBXDVoquuiyBf8SKBwrYseYdtq9j2v2nttPpE8qbuW3sE2MCk" + + "FPhTq", + }, { + Purpose: waddrmgr.KeyScopeBIP0084.Purpose, + // We always use the mainnet coin type for our BIP49/84 + // addresses! + CoinType: 0, + Account: 0, + Xpub: "tpubDDWAWrSLRSFrG1KdqXMQQyTKYGSKLKaY7gxpvK7RdV3e3Dkhvu" + + "W2GgsFvsPN4RGmuoYtUgZ1LHZE8oftz7T4mzc1BxGt5rt8zJcVQi" + + "KTPPV", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyMultiSig), + Xpub: "tpubDDXFHr67Ro2tHKVWG2gNjjijKUH1Lyv5NKFYdJnuaLGVNBVwyV" + + "5AbykhR43iy8wYozEMbw2QfmAqZhb8gnuL5mm9sZh8YsR6FjGAbe" + + "w1xoT", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyRevocationBase), + Xpub: "tpubDDXFHr67Ro2tKkccDqNfDqZpd5wCs2n6XRV2Uh185DzCTbkDaE" + + "d9v7P837zZTYBNVfaRriuxgGVgxbGjDui4CKxyzBzwz4aAZxjn2P" + + "hNcQy", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyHtlcBase), + Xpub: "tpubDDXFHr67Ro2tNH4KH41i4oTsWfRjFigoH1Ee7urvHow51opH9x" + + "J7mu1qSPMPVtkVqQZ5tE4NTuFJPrbDqno7TQietyUDmPTwyVviJb" + + "GCwXk", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyPaymentBase), + Xpub: "tpubDDXFHr67Ro2tQj5Zvav2ALhkU6dRQAhEtNPnYJVBC8hs2U1A9e" + + "cqxRY3XTiJKBDD7e8tudhmTRs8aGWJAiAXJN5kXy3Hi6cmiwGWjX" + + "K5Cv5", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyDelayBase), + Xpub: "tpubDDXFHr67Ro2tSSR2LLBJtotxx2U45cuESLWKA72YT9td3SzVKH" + + "AptzDEx5chsUNZ4WRMY5h6HJxRSebjRatxQKX1uUsux1LvKS1wsf" + + "NJ2PH", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyRevocationRoot), + Xpub: "tpubDDXFHr67Ro2tTwzfWvNoMoPpZbxdMEfe1WhbXJxvXikGixPa4g" + + "gSRZeGx6T5yxVHTVT3rjVh35Veqsowj7emX8SZfXKDKDKcLduXCe" + + "WPUU3", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyNodeKey), + Xpub: "tpubDDXFHr67Ro2tYEDS2EByRedfsUoEwBtrzVbS1qdPrX6sAkUYGL" + + "rZWvMmQv8KZDZ4zd9r8WzM9bJ2nGp7XuNVC4w2EBtWg7i76gbrmu" + + "EWjQh", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyStaticBackup), + Xpub: "tpubDDXFHr67Ro2tYpwnFJEQaM8eAPM2UV5uY6gFgXeSzS5aC5T9Tf" + + "zXuawYKBbQMZJn8qHXLafY4tAutoda1aKP5h6Nbgy3swPbnhWbFj" + + "S5wnX", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyTowerSession), + Xpub: "tpubDDXFHr67Ro2tddKpAjUegXqt7EGxRXnHkeLbUkfuFMGbLJYgRp" + + "G4ew5pMmGg2nmcGmHFQ29w3juNhd8N5ZZ8HwJdymC4f5ukQLJ4yg" + + "9rEr3", + }, { + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: uint32(keychain.KeyFamilyTowerID), + Xpub: "tpubDDXFHr67Ro2tgE89V8ZdgMytC2Jq1iT9ttGhdzR1X7haQJNBmX" + + "t8kau6taC6DGASYzbrjmo9z9w6JQFcaLNqbhS2h2PVSzKf79j265" + + "Zi8hF", + }} +) + +// testRemoteSigner tests that a watch-only wallet can use a remote signing +// wallet to perform any signing or ECDH operations. +func testRemoteSigner(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + + subTests := []struct { + name string + randomSeed bool + sendCoins bool + fn func(tt *harnessTest, wo, carol *lntest.HarnessNode) + }{{ + name: "random seed", + randomSeed: true, + fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { + // Nothing more to test here. + }, + }, { + name: "account import", + fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { + runWalletImportAccountScenario( + net, tt, + walletrpc.AddressType_WITNESS_PUBKEY_HASH, + carol, wo, + ) + }, + }, { + name: "basic channel open close", + sendCoins: true, + fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { + runBasicChannelCreationAndUpdates( + net, tt, wo, carol, + ) + }, + }, { + name: "async payments", + sendCoins: true, + fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { + runAsyncPayments(net, tt, wo, carol) + }, + }, { + name: "shared key", + fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { + runDeriveSharedKey(tt, wo) + }, + }, { + name: "cpfp", + sendCoins: true, + fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { + runCPFP(net, tt, wo, carol) + }, + }, { + name: "psbt", + fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { + runPsbtChanFunding(net, tt, carol, wo) + }, + }} + + for _, st := range subTests { + subTest := st + + // Signer is our signing node and has the wallet with the full + // master private key. We test that we can create the watch-only + // wallet from the exported accounts but also from a static key + // to make sure the derivation of the account public keys is + // correct in both cases. + password := []byte("itestpassword") + var ( + signerNodePubKey = nodePubKey + watchOnlyAccounts = accounts + signer *lntest.HarnessNode + err error + ) + if !subTest.randomSeed { + signer, err = net.RestoreNodeWithSeed( + "Signer", nil, password, nil, rootKey, 0, nil, + ) + require.NoError(t.t, err) + } else { + signer = net.NewNode(t.t, "Signer", nil) + signerNodePubKey = signer.PubKeyStr + + rpcAccts, err := signer.WalletKitClient.ListAccounts( + ctxb, &walletrpc.ListAccountsRequest{}, + ) + require.NoError(t.t, err) + + watchOnlyAccounts, err = walletrpc.AccountsToWatchOnly( + rpcAccts.Accounts, + ) + require.NoError(t.t, err) + } + + // WatchOnly is the node that has a watch-only wallet and uses + // the Signer node for any operation that requires access to + // private keys. + watchOnly, err := net.NewNodeRemoteSigner( + "WatchOnly", []string{ + "--remotesigner.enable", + fmt.Sprintf( + "--remotesigner.rpchost=localhost:%d", + signer.Cfg.RPCPort, + ), + fmt.Sprintf( + "--remotesigner.tlscertpath=%s", + signer.Cfg.TLSCertPath, + ), + fmt.Sprintf( + "--remotesigner.macaroonpath=%s", + signer.Cfg.AdminMacPath, + ), + }, password, &lnrpc.WatchOnly{ + MasterKeyBirthdayTimestamp: 0, + MasterKeyFingerprint: nil, + Accounts: watchOnlyAccounts, + }, + ) + require.NoError(t.t, err) + + resp, err := watchOnly.GetInfo(ctxb, &lnrpc.GetInfoRequest{}) + require.NoError(t.t, err) + + require.Equal(t.t, signerNodePubKey, resp.IdentityPubkey) + + if subTest.sendCoins { + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, watchOnly) + assertAccountBalance( + t.t, signer, "default", + btcutil.SatoshiPerBitcoin, 0, + ) + assertAccountBalance( + t.t, watchOnly, "default", + btcutil.SatoshiPerBitcoin, 0, + ) + } + + carol := net.NewNode(t.t, "carol", nil) + net.EnsureConnected(t.t, watchOnly, carol) + + success := t.t.Run(subTest.name, func(tt *testing.T) { + ht := newHarnessTest(tt, net) + subTest.fn(ht, watchOnly, carol) + }) + + shutdownAndAssert(net, t, carol) + shutdownAndAssert(net, t, watchOnly) + shutdownAndAssert(net, t, signer) + + if !success { + return + } + } +} diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index cd978050..b5067433 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -355,4 +355,8 @@ var allTestCases = []*testCase{ name: "wipe forwarding packages", test: testWipeForwardingPackages, }, + { + name: "remote signer", + test: testRemoteSigner, + }, }