Skip to content

Commit

Permalink
Merge d10c23b into 15f812b
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Jun 9, 2018
2 parents 15f812b + d10c23b commit 06febf9
Show file tree
Hide file tree
Showing 11 changed files with 846 additions and 471 deletions.
63 changes: 62 additions & 1 deletion cmd/lncli/commands.go
Expand Up @@ -1064,7 +1064,26 @@ var createCommand = cli.Command{
was provided by the user. This should be written down as it can be used
to potentially recover all on-chain funds, and most off-chain funds as
well.
If the --stateless_init flag is set, no macaroon files are created by
the daemon. Instead, the binary serialized admin macaroon is returned
in the answer. This answer MUST then be stored somewhere, otherwise
all access to the RPC server will be lost and the wallet must be re-
created to re-gain access. If the --save_to parameter is set, the
macaroon is saved to this file, otherwise it is printed to standard out.
`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "stateless_init",
Usage: "return the admin macaroon instead of " +
"creating files in the file system of the " +
"daemon",
},
cli.StringFlag{
Name: "save_to",
Usage: "save returned admin macaroon to this file",
},
},
Action: actionDecorator(create),
}

Expand Down Expand Up @@ -1100,6 +1119,24 @@ func create(ctx *cli.Context) error {
client, cleanUp := getWalletUnlockerClient(ctx)
defer cleanUp()

// Should the daemon be initialized stateless? Then we expect an answer
// with the admin macaroon later.
statelessInit := ctx.IsSet("stateless_init") &&
ctx.Bool("stateless_init")

// If the stateless init flag is set, there can be an optional
// --save_to parameter that specifies where the returned macaroon
// should be saved to.
var macSavePath string
if ctx.IsSet("save_to") {
if statelessInit {
macSavePath = cleanAndExpandPath(ctx.String("save_to"))
} else {
return fmt.Errorf("cannot set save_to parameter " +
"without stateless_init")
}
}

// First, we'll prompt the user for their passphrase twice to ensure
// both attempts match up properly.
fmt.Printf("Input wallet password: ")
Expand Down Expand Up @@ -1306,13 +1343,37 @@ mnemonicCheck:
CipherSeedMnemonic: cipherSeedMnemonic,
AezeedPassphrase: aezeedPass,
RecoveryWindow: recoveryWindow,
StatelessInit: statelessInit,
}
if _, err := client.InitWallet(ctxb, req); err != nil {
response, err := client.InitWallet(ctxb, req)
if err != nil {
return err
}

fmt.Println("\nlnd successfully initialized!")

// If stateless initialization is requested, we expect a macaroon
// in the response. The user MUST store this macaroon somewhere so we
// either save it to a provided file path or just print it to standard
// output.
if statelessInit {
if macSavePath != "" {
err = ioutil.WriteFile(
macSavePath, response.AdminMacaroon, 0644,
)
if err != nil {
os.Remove(macSavePath)
return err
}
fmt.Printf("Admin macaroon saved to %s\n", macSavePath)
} else {
fmt.Printf(
"Admin macaroon: %s\n",
hex.EncodeToString(response.AdminMacaroon),
)
}
}

return nil
}

Expand Down
104 changes: 65 additions & 39 deletions lnd.go
Expand Up @@ -196,10 +196,11 @@ func lndMain() error {
proxyOpts := []grpc.DialOption{grpc.WithTransportCredentials(cCreds)}

var (
privateWalletPw = lnwallet.DefaultPrivatePassphrase
publicWalletPw = lnwallet.DefaultPublicPassphrase
birthday time.Time
recoveryWindow uint32
privateWalletPw = lnwallet.DefaultPrivatePassphrase
publicWalletPw = lnwallet.DefaultPublicPassphrase
birthday time.Time
recoveryWindow uint32
macaroonResponse chan []byte
)

// We wait until the user provides a password over RPC. In case lnd is
Expand All @@ -218,6 +219,7 @@ func lndMain() error {
publicWalletPw = walletInitParams.Password
birthday = walletInitParams.Birthday
recoveryWindow = walletInitParams.RecoveryWindow
macaroonResponse = walletInitParams.MacaroonResponseChannel

if recoveryWindow > 0 {
ltndLog.Infof("Wallet recovery mode enabled with "+
Expand All @@ -238,25 +240,43 @@ func lndMain() error {
defer macaroonService.Close()

// Try to unlock the macaroon store with the private password.
// Ignore ErrAlreadyUnlocked since it could be unlocked by the
// wallet unlocker.
err = macaroonService.CreateUnlock(&privateWalletPw)
if err != nil {
if err != nil && err != macaroons.ErrAlreadyUnlocked {
srvrLog.Error(err)
return err
}

// Create macaroon files for lncli to use if they don't exist.
if !fileExists(cfg.AdminMacPath) && !fileExists(cfg.ReadMacPath) &&
!fileExists(cfg.InvoiceMacPath) {

err = genMacaroons(
ctx, macaroonService, cfg.AdminMacPath,
cfg.ReadMacPath, cfg.InvoiceMacPath,
// If the macaroon response channel is not nil, it means that
// the user requested a stateless initialization, where no
// macaroon files should be created but instead the admin
// macaroon returned in the InitWallet response.
if macaroonResponse != nil {
adminMacBytes, err := bakeMacaroon(
ctx, macaroonService, adminPermissions,
)
if err != nil {
ltndLog.Errorf("unable to create macaroon "+
"files: %v", err)
return err
}
macaroonResponse <- adminMacBytes
} else {
// Create macaroon files for lncli to use if they don't
// exist.
if !fileExists(cfg.AdminMacPath) &&
!fileExists(cfg.ReadMacPath) &&
!fileExists(cfg.InvoiceMacPath) {

err = genMacaroons(
ctx, macaroonService, cfg.AdminMacPath,
cfg.ReadMacPath, cfg.InvoiceMacPath,
)
if err != nil {
ltndLog.Errorf("unable to create " +
"macaroon files: %v", err)
return err
}
}
}
}

Expand Down Expand Up @@ -795,6 +815,24 @@ func genCertPair(certFile, keyFile string) error {
return nil
}

// bakeMacaroon creates a new macaroon with newest version and the given
// permissions then returns it binary serialized.
func bakeMacaroon(ctx context.Context, svc *macaroons.Service,
permissions []bakery.Op) ([]byte, error) {
mac, err := svc.Oven.NewMacaroon(
ctx, bakery.LatestVersion, nil, permissions...,
)
if err != nil {
return nil, err
}
macBytes, err := mac.M().MarshalBinary()
if err != nil {
return nil, err
}

return macBytes, nil
}

// genMacaroons generates three macaroon files; one admin-level, one
// for invoice access and one read-only. These can also be used
// to generate more granular macaroons.
Expand All @@ -805,13 +843,7 @@ func genMacaroons(ctx context.Context, svc *macaroons.Service,
// access invoice related calls. This is useful for merchants and other
// services to allow an isolated instance that can only query and
// modify invoices.
invoiceMac, err := svc.Oven.NewMacaroon(
ctx, bakery.LatestVersion, nil, invoicePermissions...,
)
if err != nil {
return err
}
invoiceMacBytes, err := invoiceMac.M().MarshalBinary()
invoiceMacBytes, err := bakeMacaroon(ctx, svc, invoicePermissions)
if err != nil {
return err
}
Expand All @@ -822,13 +854,7 @@ func genMacaroons(ctx context.Context, svc *macaroons.Service,
}

// Generate the read-only macaroon and write it to a file.
roMacaroon, err := svc.Oven.NewMacaroon(
ctx, bakery.LatestVersion, nil, readPermissions...,
)
if err != nil {
return err
}
roBytes, err := roMacaroon.M().MarshalBinary()
roBytes, err := bakeMacaroon(ctx, svc, readPermissions)
if err != nil {
return err
}
Expand All @@ -838,14 +864,7 @@ func genMacaroons(ctx context.Context, svc *macaroons.Service,
}

// Generate the admin macaroon and write it to a file.
adminPermissions := append(readPermissions, writePermissions...)
admMacaroon, err := svc.Oven.NewMacaroon(
ctx, bakery.LatestVersion, nil, adminPermissions...,
)
if err != nil {
return err
}
admBytes, err := admMacaroon.M().MarshalBinary()
admBytes, err := bakeMacaroon(ctx, svc, adminPermissions)
if err != nil {
return err
}
Expand All @@ -869,6 +888,12 @@ type WalletUnlockParams struct {
// RecoveryWindow specifies the address lookahead when entering recovery
// mode. A recovery will be attempted if this value is non-zero.
RecoveryWindow uint32

// MacaroonResponseChannel is an optional channel for sending back
// the admin macaroon to the WalletUnlocker service.
// If the channel is not nil, the service expects a message on the
// channel. Otherwise it means no stateless initialization is requested.
MacaroonResponseChannel chan []byte
}

// waitForWalletPassword will spin up gRPC and REST endpoints for the
Expand Down Expand Up @@ -1010,9 +1035,10 @@ func waitForWalletPassword(grpcEndpoints, restEndpoints []string,
}

walletInitParams := &WalletUnlockParams{
Password: password,
Birthday: birthday,
RecoveryWindow: recoveryWindow,
Password: password,
Birthday: birthday,
RecoveryWindow: recoveryWindow,
MacaroonResponseChannel: initMsg.MacaroonResponseChannel,
}

return walletInitParams, nil
Expand Down
76 changes: 75 additions & 1 deletion lnd_test.go
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/roasbeef/btcutil"
"golang.org/x/net/context"
"google.golang.org/grpc"
"gopkg.in/macaroon.v2"
)

var (
Expand Down Expand Up @@ -457,7 +458,9 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) {
// used for key derivation. This will bring up Carol with an empty
// wallet, and such that she is synced up.
password := []byte("The Magic Words are Squeamish Ossifrage")
carol, mnemonic, err := net.NewNodeWithSeed("Carol", nil, password)
carol, mnemonic, _, err := net.NewNodeWithSeed(
"Carol", nil, password, false,
)
if err != nil {
t.Fatalf("unable to create node with seed; %v", err)
}
Expand Down Expand Up @@ -9697,6 +9700,73 @@ func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) {
}
}

// testStatelessInit checks that the stateless initialization of the daemon
// does not write any macaroon files to the daemon's file system and returns
// the admin macaroon in the response.
func testStatelessInit(net *lntest.NetworkHarness, t *harnessTest) {
var (
ctxb = context.Background()
timeout = time.Duration(time.Second * 15)
ctxt, _ = context.WithTimeout(ctxb, timeout)
newAddrReq = &lnrpc.NewWitnessAddressRequest{}
)

// First, create a new node and request it to initialize stateless.
// This should return us the binary serialized admin macaroon that we
// can then use for further calls.
carol, _, macBytes, err := net.NewNodeWithSeed(
"Carol", nil, []byte("stateless"), true,
)
if err != nil {
t.Fatalf("unable to create node with seed; %v", err)
}
if macBytes == nil || len(macBytes) == 0 {
t.Fatalf("invalid macaroon returned in stateless init")
}

// Now make sure no macaroon files have been created by the node Carol.
if _, err := os.Stat(carol.AdminMacPath()); err == nil {
t.Fatalf(
"unexpected macaroon file in stateless init: %s",
carol.AdminMacPath(),
)
}
if _, err := os.Stat(carol.ReadMacPath()); err == nil {
t.Fatalf(
"unexpected macaroon file in stateless init: %s",
carol.ReadMacPath(),
)
}
if _, err := os.Stat(carol.InvoiceMacPath()); err == nil {
t.Fatalf(
"unexpected macaroon file in stateless init: %s",
carol.InvoiceMacPath(),
)
}

// Then check that we can unmarshal the binary serialized macaroon.
adminMac := &macaroon.Macaroon{}
if err = adminMac.UnmarshalBinary(macBytes); err != nil {
t.Fatalf("unable to unmarshal macaroon: %v", err)
}

// Finally, test that we can actually use the macaroon that has been
// returned to us for a RPC call.
conn, err := carol.ConnectRPCWithMacaroon(adminMac)
if err != nil {
t.Fatalf("unable to connect to alice: %v", err)
}
defer conn.Close()
adminMacConnection := lnrpc.NewLightningClient(conn)
res, err := adminMacConnection.NewWitnessAddress(ctxt, newAddrReq)
if err != nil {
t.Fatalf("unable to get new address with macaroon: %v", err)
}
if !strings.HasPrefix(res.Address, "r") {
t.Fatalf("returned address was not a regtest address")
}
}

type testCase struct {
name string
test func(net *lntest.NetworkHarness, t *harnessTest)
Expand Down Expand Up @@ -9888,6 +9958,10 @@ var testsCases = []*testCase{
name: "query routes",
test: testQueryRoutes,
},
{
name: "stateless init",
test: testStatelessInit,
},
}

// TestLightningNetworkDaemon performs a series of integration tests amongst a
Expand Down

0 comments on commit 06febf9

Please sign in to comment.