diff --git a/server/cmd/arkd/main.go b/server/cmd/arkd/main.go index 796a687..116ec59 100755 --- a/server/cmd/arkd/main.go +++ b/server/cmd/arkd/main.go @@ -79,19 +79,21 @@ func mainAction(_ *cli.Context) error { BitcoindRpcPass: cfg.BitcoindRpcPass, BitcoindRpcHost: cfg.BitcoindRpcHost, BoardingExitDelay: cfg.BoardingExitDelay, + UnlockerType: cfg.UnlockerType, + UnlockerFilePath: cfg.UnlockerFilePath, } svc, err := grpcservice.NewService(svcConfig, appConfig) if err != nil { return err } - log.RegisterExitHandler(svc.Stop) - log.Info("starting service...") if err := svc.Start(); err != nil { return err } + log.RegisterExitHandler(svc.Stop) + sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, os.Interrupt) <-sigChan diff --git a/server/internal/app-config/config.go b/server/internal/app-config/config.go index 863418e..5e69960 100644 --- a/server/internal/app-config/config.go +++ b/server/internal/app-config/config.go @@ -11,6 +11,7 @@ import ( scheduler "github.com/ark-network/ark/server/internal/infrastructure/scheduler/gocron" txbuilder "github.com/ark-network/ark/server/internal/infrastructure/tx-builder/covenant" cltxbuilder "github.com/ark-network/ark/server/internal/infrastructure/tx-builder/covenantless" + fileunlocker "github.com/ark-network/ark/server/internal/infrastructure/unlocker/file" btcwallet "github.com/ark-network/ark/server/internal/infrastructure/wallet/btc-embedded" liquidwallet "github.com/ark-network/ark/server/internal/infrastructure/wallet/liquid-standalone" log "github.com/sirupsen/logrus" @@ -37,6 +38,9 @@ var ( "ocean": {}, "btcwallet": {}, } + supportedUnlockers = supportedType{ + "file": {}, + } supportedNetworks = supportedType{ common.Bitcoin.Name: {}, common.BitcoinTestNet.Name: {}, @@ -70,6 +74,9 @@ type Config struct { BitcoindRpcPass string BitcoindRpcHost string + UnlockerType string + UnlockerFilePath string + repo ports.RepoManager svc application.Service adminSvc application.AdminService @@ -77,6 +84,7 @@ type Config struct { txBuilder ports.TxBuilder scanner ports.BlockchainScanner scheduler ports.SchedulerService + unlocker ports.Unlocker } func (c *Config) Validate() error { @@ -95,6 +103,9 @@ func (c *Config) Validate() error { if !supportedScanners.supports(c.BlockchainScannerType) { return fmt.Errorf("blockchain scanner type not supported, please select one of: %s", supportedScanners) } + if len(c.UnlockerType) > 0 && !supportedUnlockers.supports(c.UnlockerType) { + return fmt.Errorf("unlocker type not supported, please select one of: %s", supportedUnlockers) + } if c.RoundInterval < 2 { return fmt.Errorf("invalid round interval, must be at least 2 seconds") } @@ -151,7 +162,7 @@ func (c *Config) Validate() error { return err } if err := c.walletService(); err != nil { - return fmt.Errorf("failed to connect to wallet: %s", err) + return err } if err := c.txBuilderService(); err != nil { return err @@ -165,6 +176,9 @@ func (c *Config) Validate() error { if err := c.adminService(); err != nil { return err } + if err := c.unlockerService(); err != nil { + return err + } return nil } @@ -185,6 +199,10 @@ func (c *Config) WalletService() ports.WalletService { return c.wallet } +func (c *Config) UnlockerService() ports.Unlocker { + return c.unlocker +} + func (c *Config) repoManager() error { var svc ports.RepoManager var err error @@ -227,7 +245,7 @@ func (c *Config) walletService() error { if common.IsLiquid(c.Network) { svc, err := liquidwallet.NewService(c.WalletAddr) if err != nil { - return err + return fmt.Errorf("failed to connect to wallet: %s", err) } c.wallet = svc @@ -353,6 +371,26 @@ func (c *Config) adminService() error { return nil } +func (c *Config) unlockerService() error { + if len(c.UnlockerType) <= 0 { + return nil + } + + var svc ports.Unlocker + var err error + switch c.UnlockerType { + case "file": + svc, err = fileunlocker.NewService(c.UnlockerFilePath) + default: + err = fmt.Errorf("unknown unlocker type") + } + if err != nil { + return err + } + c.unlocker = svc + return nil +} + type supportedType map[string]struct{} func (t supportedType) String() string { diff --git a/server/internal/config/config.go b/server/internal/config/config.go index eb12329..ba44727 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -36,6 +36,8 @@ type Config struct { BitcoindRpcHost string TLSExtraIPs []string TLSExtraDomains []string + UnlockerType string + UnlockerFilePath string } var ( @@ -59,12 +61,14 @@ var ( // #nosec G101 BitcoindRpcUser = "BITCOIND_RPC_USER" // #nosec G101 - BitcoindRpcPass = "BITCOIND_RPC_PASS" - BitcoindRpcHost = "BITCOIND_RPC_HOST" - NoMacaroons = "NO_MACAROONS" - NoTLS = "NO_TLS" - TLSExtraIP = "TLS_EXTRA_IP" - TLSExtraDomain = "TLS_EXTRA_DOMAIN" + BitcoindRpcPass = "BITCOIND_RPC_PASS" + BitcoindRpcHost = "BITCOIND_RPC_HOST" + NoMacaroons = "NO_MACAROONS" + NoTLS = "NO_TLS" + TLSExtraIP = "TLS_EXTRA_IP" + TLSExtraDomain = "TLS_EXTRA_DOMAIN" + UnlockerType = "UNLOCKER_TYPE" + UnlockerFilePath = "UNLOCKER_FILE_PATH" defaultDatadir = common.AppDataDir("arkd", false) defaultRoundInterval = 5 @@ -142,6 +146,8 @@ func LoadConfig() (*Config, error) { NoMacaroons: viper.GetBool(NoMacaroons), TLSExtraIPs: viper.GetStringSlice(TLSExtraIP), TLSExtraDomains: viper.GetStringSlice(TLSExtraDomain), + UnlockerType: viper.GetString(UnlockerType), + UnlockerFilePath: viper.GetString(UnlockerFilePath), }, nil } diff --git a/server/internal/core/ports/unlocker.go b/server/internal/core/ports/unlocker.go new file mode 100644 index 0000000..7d5efa9 --- /dev/null +++ b/server/internal/core/ports/unlocker.go @@ -0,0 +1,7 @@ +package ports + +import "context" + +type Unlocker interface { + GetPassword(ctx context.Context) (string, error) +} diff --git a/server/internal/infrastructure/unlocker/file/service.go b/server/internal/infrastructure/unlocker/file/service.go new file mode 100644 index 0000000..65cca84 --- /dev/null +++ b/server/internal/infrastructure/unlocker/file/service.go @@ -0,0 +1,37 @@ +package fileunlocker + +import ( + "bytes" + "context" + "fmt" + "os" + + "github.com/ark-network/ark/server/internal/core/ports" +) + +type service struct { + filePath string +} + +func NewService(filePath string) (ports.Unlocker, error) { + if _, err := os.Stat(filePath); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("password file not found at path %s", filePath) + } + return nil, err + } + return &service{filePath: filePath}, nil +} + +func (s *service) GetPassword(_ context.Context) (string, error) { + buf, err := os.ReadFile(s.filePath) + if err != nil { + return "", err + } + + password := bytes.TrimFunc(buf, func(r rune) bool { + return r == 10 || r == 13 || r == 32 + }) + + return string(password), nil +} diff --git a/server/internal/infrastructure/wallet/btc-embedded/wallet.go b/server/internal/infrastructure/wallet/btc-embedded/wallet.go index 9d7c2fc..cea1923 100644 --- a/server/internal/infrastructure/wallet/btc-embedded/wallet.go +++ b/server/internal/infrastructure/wallet/btc-embedded/wallet.go @@ -304,6 +304,10 @@ func (s *service) Restore(_ context.Context, seed, password string) error { } func (s *service) Unlock(_ context.Context, password string) error { + if !s.walletInitialized() { + return fmt.Errorf("wallet not initialized") + } + if !s.walletLoaded() { pwd := []byte(password) opt := btcwallet.LoaderWithLocalWalletDB(s.cfg.Datadir, false, time.Minute) diff --git a/server/internal/interface/grpc/service.go b/server/internal/interface/grpc/service.go index fb7841d..2d15c6f 100644 --- a/server/internal/interface/grpc/service.go +++ b/server/internal/interface/grpc/service.go @@ -94,7 +94,13 @@ func NewService( func (s *service) Start() error { withoutAppSvc := false - return s.start(withoutAppSvc) + if err := s.start(withoutAppSvc); err != nil { + return err + } + if s.appConfig.UnlockerService() != nil { + return s.autoUnlock() + } + return nil } func (s *service) Stop() { @@ -314,6 +320,33 @@ func (s *service) onInit(password string) { log.Debugf("generated macaroons at path %s", datadir) } +func (s *service) autoUnlock() error { + ctx := context.Background() + wallet := s.appConfig.WalletService() + + status, err := wallet.Status(ctx) + if err != nil { + return fmt.Errorf("failed to get wallet status: %s", err) + } + if !status.IsInitialized() { + log.Debug("wallet not initiialized, skipping auto unlock") + return nil + } + + password, err := s.appConfig.UnlockerService().GetPassword(ctx) + if err != nil { + return fmt.Errorf("failed to get password: %s", err) + } + if err := wallet.Unlock(ctx, password); err != nil { + return fmt.Errorf("failed to auto unlock: %s", err) + } + + go s.onUnlock(password) + + log.Debug("service auto unlocked") + return nil +} + func router( grpcServer *grpc.Server, grpcGateway http.Handler, ) http.Handler {