Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add remote signing binary #9293

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
de5b1c1
lnd+lncfg: add outbound remote signer to config
ViktorTigerstrom May 14, 2024
759b817
lncfg: correct `DefaultRemoteSignerRPCTimeout` docs
ViktorTigerstrom May 20, 2024
33b8286
lnd: add new `remotesigner` macaroon entity
ViktorTigerstrom Aug 23, 2024
0ab64ff
walletrpc: add `SignCoordinatorStreams` RPC
ViktorTigerstrom May 14, 2024
7643663
rpcwallet: add `RemoteSigner` interface
ViktorTigerstrom May 14, 2024
80b0ad3
rpcwallet: add InboundRemoteSigner implementation
ViktorTigerstrom May 14, 2024
c375590
rpcwallet: add `RemoteSignerBuilder`
ViktorTigerstrom May 14, 2024
22173bb
rpcwallet: use `RemoteSigner` in RPCKeyRing
ViktorTigerstrom May 14, 2024
27cb7eb
lnd+rpcwallet: use `RemoteSigner` for health check
ViktorTigerstrom May 14, 2024
96569ea
rpcwallet: add `RemoteSignerClient` struct
ViktorTigerstrom May 14, 2024
e469852
f - rpcwallet: use GoroutineManager in remote signer signer client
ViktorTigerstrom Oct 31, 2024
66d22b3
rpcwallet: Add `RemoteSignerClientBuilder`
ViktorTigerstrom Sep 1, 2024
748c28a
lnd: add RemoteSignerClient instance on startup
ViktorTigerstrom May 14, 2024
16fdc25
lncfg: enable signerrole `signer-outbound`
ViktorTigerstrom May 14, 2024
bc1a52b
rpcwallet: add `SignCoordinator` struct
ViktorTigerstrom May 14, 2024
6257910
rpcwallet: add OutboundRemoteSigner implementation
ViktorTigerstrom May 14, 2024
d252d69
lnrpc: add AllowRemoteSigner WalletState proto
ViktorTigerstrom May 14, 2024
300047b
rpcperms: allow some RPCs before rpcActive state
ViktorTigerstrom May 14, 2024
3173439
rpcperms: fix SetServerActive function docs typo
ViktorTigerstrom May 14, 2024
73ee337
multi: enable RpcServer before dependencies exist
ViktorTigerstrom May 14, 2024
71f512c
multi: add `RemoteSigner` to walletrpc config
ViktorTigerstrom May 14, 2024
2213b95
walletrpc: implement `SignCoordinatorStreams` RPC
ViktorTigerstrom May 14, 2024
3f1bd7c
multi: add RemoteSigner before other dependencies
ViktorTigerstrom May 28, 2024
6694892
multi: add `ReadySignal` to `WalletController`
ViktorTigerstrom May 14, 2024
0221e2d
lnd: await remote signer connection on startup
ViktorTigerstrom May 28, 2024
5d51427
multi: enable signerrole `watchonly-outbound`
ViktorTigerstrom May 14, 2024
8d4233c
docs: add outbound signer to remote signing docs
ViktorTigerstrom May 13, 2024
1ce108a
docs: update release notes
ViktorTigerstrom Oct 31, 2024
ebe7025
lntest: separate creation/start of watch-only node
ViktorTigerstrom May 14, 2024
c9fcf12
itest: add outbound remote signer itest
ViktorTigerstrom May 14, 2024
bfdd795
itest: add testOutboundRSMacaroonEnforcement itest
ViktorTigerstrom Aug 28, 2024
5d6e40b
itest: wrap deriveCustomScopeAccounts at 80 chars
ViktorTigerstrom May 17, 2024
fd933d6
lncfg: Add `signer-inbound` `signerrole`
ViktorTigerstrom Nov 21, 2024
a563368
multi: Block non-whitelisted RPCs as remote signer
ViktorTigerstrom Nov 21, 2024
55d16b7
docs: recommend setting signer-inbound signerrole
ViktorTigerstrom Nov 21, 2024
c7784e4
multi: Add `lndsigner` binary
ViktorTigerstrom Nov 21, 2024
570b34f
lnd+lncfg: Add `SignerConfig`
ViktorTigerstrom Nov 21, 2024
2b03e06
lnd: load `SignerConfig` into `lndsigner`
ViktorTigerstrom Nov 21, 2024
76e4caf
f - lnd: Use interface for config loading instead
ViktorTigerstrom Nov 20, 2024
37e2ffc
make+scripts: add `lndsigner` to release script
ViktorTigerstrom Nov 21, 2024
bc0c0dc
docs: add `lndsigner` info to remote signing docs
ViktorTigerstrom Nov 21, 2024
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
Prev Previous commit
Next Next commit
lnd: load SignerConfig into lndsigner
This commit introduces the functionality to load the `SignerConfig` from
the configuration file into `lndsigner`. The goal of `lndsigner` is to
remain compatible with `lnd`'s `Main` function in `lnd.go` while serving
as a stripped-down version of `lnd` with a simplified user experience
during setup.

To achieve this, the `SignerConfig` must be merged with the main
`Config` struct upon loading. This commit implements that functionality.
  • Loading branch information
ViktorTigerstrom committed Nov 22, 2024
commit 2b03e065e90fecba1a09cf9b2b65ead6ab7e804c
225 changes: 182 additions & 43 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ const (
bitcoindBackendName = "bitcoind"
btcdBackendName = "btcd"
neutrinoBackendName = "neutrino"
noChainBackendName = "nochainbackend"
)

var (
Expand Down Expand Up @@ -749,6 +750,13 @@ func DefaultConfig() Config {

// SignerConfig defines the configuration options for lndsigner.
//
// Note! Any new fields added to this struct MUST also be applied to the merged
// config in the `mergeConf` function in LoadSignerConfig. Else the added fields
// will have no effect.
//
// See LoadSignerConfig for further details regarding the configuration
// loading+parsing process.
//
//nolint:lll
type SignerConfig struct {
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
Expand Down Expand Up @@ -813,9 +821,153 @@ func DefaultSignerConfig() SignerConfig {
// 3. Load configuration file overwriting defaults with any specified options
// 4. Parse CLI options and overwrite/add any specified options
func LoadConfig(interceptor signal.Interceptor) (*Config, error) {
// As we're passing the default Config type to generalizedConfigLoader,
// no further modification is required to the loaded config during
// merging.
mergeConf := func(cfg *Config) (*Config, error) {
return cfg, nil
}

getShowVersion := func(cfg *Config) bool {
return cfg.ShowVersion
}

getLndDir := func(cfg *Config) string {
return cfg.LndDir
}

getConfigPath := func(cfg *Config) string {
return cfg.ConfigFile
}

preCfg := DefaultConfig()

// Load and validate the config.
cfg, err := generalizedConfigLoader(
interceptor, preCfg, mergeConf, getShowVersion, getLndDir,
getConfigPath, DefaultConfigFile, lncfg.DefaultConfigFilename,
)

return cfg, err
}

// LoadSignerConfig initializes and parses the remote signer config using a
// config file and command line options.
//
// The configuration proceeds as follows:s
// 1. Start with a default signer config with sane settings
// 2. Overwrite with signer specific values
// 3. Pre-parse the command line to check for an alternative config file
// 4. Parse CLI options and overwrite/add any specified options
// 5. Load a main lnd config type, and merge the signer configuration file with
// the main config file.
func LoadSignerConfig(interceptor signal.Interceptor) (*Config, error) {
// We'll merge the configs by copying the values from the SignerConfig
// to the corresponding field in the main Config type.
mergeConf := func(signerCfg *SignerConfig) (*Config, error) {
cfg := DefaultConfig()

// First we'll overwrite the default config with values that
// should be set when a node acts as a remote signer.
cfg.NoNetBootstrap = true
cfg.DisableListen = true
cfg.Bitcoin.Node = noChainBackendName
cfg.ConfigFile = DefaultSignerConfigFile

// Next, we'll copy over the values that are set in the signer
// config, to merge them with the main config.
cfg.LndDir = signerCfg.LndDir
cfg.ConfigFile = signerCfg.ConfigFile
cfg.DataDir = signerCfg.DataDir

cfg.DebugLevel = signerCfg.DebugLevel
cfg.TLSCertPath = signerCfg.TLSCertPath
cfg.TLSKeyPath = signerCfg.TLSKeyPath
cfg.LogDir = signerCfg.LogDir
cfg.LogConfig = signerCfg.LogConfig
cfg.RPCMiddleware = signerCfg.RPCMiddleware

cfg.RawRPCListeners = signerCfg.RawRPCListeners
cfg.RawRESTListeners = signerCfg.RawRESTListeners
cfg.RawListeners = signerCfg.RawListeners

cfg.RemoteSigner.SignerRole = signerCfg.SignerRole
cfg.RemoteSigner.Timeout = signerCfg.Timeout
cfg.RemoteSigner.RequestTimeout = signerCfg.RequestTimeout

cfg.RemoteSigner.RPCHost = signerCfg.WatchOnlyRPCHost
cfg.RemoteSigner.MacaroonPath = signerCfg.WatchOnlyMacaroonPath
cfg.RemoteSigner.TLSCertPath = signerCfg.WatchOnlyTLSCertPath

cfg.Pprof = signerCfg.Pprof

switch signerCfg.Network {
case (chainreg.BitcoinMainNetParams.Params.Name):
cfg.Bitcoin.MainNet = true
case (chainreg.BitcoinTestNetParams.Params.Name), "testnet":
cfg.Bitcoin.TestNet3 = true
case (chainreg.BitcoinRegTestNetParams.Params.Name):
cfg.Bitcoin.RegTest = true
case (chainreg.BitcoinSimNetParams.Params.Name):
cfg.Bitcoin.SimNet = true
case (chainreg.BitcoinSigNetParams.Params.Name):
cfg.Bitcoin.SigNet = true
default:
return nil, fmt.Errorf(
"unknown network %s", signerCfg.Network,
)
}

return &cfg, nil
}

getShowVersion := func(signerCfg *SignerConfig) bool {
return signerCfg.ShowVersion
}

getLndDir := func(signerCfg *SignerConfig) string {
return signerCfg.LndDir
}

getConfigPath := func(signerCfg *SignerConfig) string {
return signerCfg.ConfigFile
}

// We use the SignerConfig, as the lndsigner should have more limited
// config options for an easier UX.
preCfg := DefaultSignerConfig()

// Load and validate the config.
cfg, err := generalizedConfigLoader(
interceptor, preCfg, mergeConf, getShowVersion, getLndDir,
getConfigPath, DefaultSignerConfigFile,
lncfg.DefaultSignerConfigFilename,
)
if err != nil {
return nil, err
}

// TODO(viktor): Remove this once RPCMiddleware interception is
// supported for outbound remote signers.
if cfg.RemoteSigner.SignerRole == lncfg.OutboundSignerRole &&
cfg.RPCMiddleware.Enable {

return nil, errors.New("RPCMiddleware interception is " +
"currently not supported when using an outbound " +
"remote signer")
}

return cfg, err
}

func generalizedConfigLoader[R interface{}](interceptor signal.Interceptor,
preCfg R, mergeConfig func(cfg *R) (*Config, error),
getShowVersion func(cfg *R) bool,
getLndDir, getConfigPath func(cfg *R) string, defaultConfigPath,
defaultConfigFileName string) (*Config, error) {

// Pre-parse the command line options to pick up an alternative config
// file.
preCfg := DefaultConfig()
if _, err := flags.Parse(&preCfg); err != nil {
return nil, err
}
Expand All @@ -824,7 +976,7 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) {
appName := filepath.Base(os.Args[0])
appName = strings.TrimSuffix(appName, filepath.Ext(appName))
usageMessage := fmt.Sprintf("Use %s -h to show usage", appName)
if preCfg.ShowVersion {
if getShowVersion(&preCfg) {
fmt.Println(appName, "version", build.Version(),
"commit="+build.Commit)
os.Exit(0)
Expand All @@ -834,21 +986,21 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) {
// use the default config file path. However, if the user has modified
// their lnddir, then we should assume they intend to use the config
// file within it.
configFileDir := CleanAndExpandPath(preCfg.LndDir)
configFilePath := CleanAndExpandPath(preCfg.ConfigFile)
configFileDir := CleanAndExpandPath(getLndDir(&preCfg))
configFilePath := CleanAndExpandPath(getConfigPath(&preCfg))
switch {
// User specified --lnddir but no --configfile. Update the config file
// path to the lnd config directory, but don't require it to exist.
case configFileDir != DefaultLndDir &&
configFilePath == DefaultConfigFile:
configFilePath == defaultConfigPath:

configFilePath = filepath.Join(
configFileDir, lncfg.DefaultConfigFilename,
configFileDir, defaultConfigFileName,
)

// User did specify an explicit --configfile, so we check that it does
// exist under that path to avoid surprises.
case configFilePath != DefaultConfigFile:
case configFilePath != defaultConfigPath:
if !lnrpc.FileExists(configFilePath) {
return nil, fmt.Errorf("specified config file does "+
"not exist in %s", configFilePath)
Expand All @@ -857,8 +1009,7 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) {

// Next, load any additional configuration options from the file.
var configFileError error
cfg := preCfg
fileParser := flags.NewParser(&cfg, flags.Default)
fileParser := flags.NewParser(&preCfg, flags.Default)
err := flags.NewIniParser(fileParser).ParseFile(configFilePath)
if err != nil {
// If it's a parsing related error, then we'll return
Expand All @@ -875,14 +1026,33 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) {

// Finally, parse the remaining command line options again to ensure
// they take precedence.
flagParser := flags.NewParser(&cfg, flags.Default)
flagParser := flags.NewParser(&preCfg, flags.Default)
if _, err := flagParser.Parse(); err != nil {
return nil, err
}

// Merge the loaded config into the main Config type.
cfg, err := mergeConfig(&preCfg)
if err != nil {
return nil, err
}

// The flag parser above is only aware of the flags in the preCfg
// definition, and not necessarily those in the Config struct. To handle
// this, we create a new flag parser for the Config struct, as it is
// passed to the ValidateConfig function.
// Since some flags in preCfg may not exist in Config, we use
// flags.IgnoreUnknown here to avoid errors for those flags. However,
// unknown flags are not allowed for the preCfg itself, as the earlier
// flag parser will error if such flags are present.
mergedFlagParser := flags.NewParser(cfg, flags.IgnoreUnknown)
if _, err := mergedFlagParser.Parse(); err != nil {
return nil, err
}
Comment on lines +1065 to +1076
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you can think of a cleaner way of doing this, as this is arguably quite dirty.


// Make sure everything we just loaded makes sense.
cleanCfg, err := ValidateConfig(
cfg, interceptor, fileParser, flagParser,
*cfg, interceptor, fileParser, mergedFlagParser,
)
var usageErr *lncfg.UsageError
if errors.As(err, &usageErr) {
Expand Down Expand Up @@ -920,37 +1090,6 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) {
return cleanCfg, nil
}

// LoadSignerConfig initializes and parses the config using a config file and
// command line options.
//
// The configuration proceeds as follows:
// 1. Start with a default config with sane settings
// 2. Pre-parse the command line to check for an alternative config file
// 3. Load configuration file overwriting defaults with any specified options
// 4. Parse CLI options and overwrite/add any specified options
// 5. Hardcode that the node should not bootstrap from the network, listen
// for incoming connections, or connect to a chain backend.
func LoadSignerConfig(interceptor signal.Interceptor) (*Config, error) {
cfg, err := LoadConfig(interceptor)
if err != nil {
return nil, err
}

cfg.NoNetBootstrap = true
cfg.DisableListen = true
cfg.Bitcoin.Node = "nochainbackend"

if cfg.RemoteSigner.SignerRole != lncfg.OutboundSignerRole &&
cfg.RemoteSigner.SignerRole != lncfg.InboundSignerRole {

return nil, fmt.Errorf("signerrole must be set to either "+
"%s or %s for lndsigner", lncfg.OutboundSignerRole,
lncfg.InboundSignerRole)
}

return cfg, nil
}

// ValidateConfig check the given configuration to be sane. This makes sure no
// illegal values or combination of values are set. All file system paths are
// normalized. The cleaned up config is returned on success.
Expand Down Expand Up @@ -1395,7 +1534,7 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
case neutrinoBackendName:
// No need to get RPC parameters.

case "nochainbackend":
case noChainBackendName:
// Nothing to configure, we're running without any chain
// backend whatsoever (pure signing mode).

Expand Down