From 72e31d839a76495024ef2eed285b4673aabc0729 Mon Sep 17 00:00:00 2001 From: Louis Singer <41042567+louisinger@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:47:42 +0200 Subject: [PATCH] Fix btc wallet restore (covenantless asp) (#332) * first account = default btcwallet account (account index 0) * Update server/internal/infrastructure/wallet/btc-embedded/wallet.go Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com> Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com> * fix restoration * increase arkd timeout * fix connector signature --------- Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com> Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com> --- common/fees.go | 6 +- server/cmd/arkd/commands.go | 10 +- .../wallet/btc-embedded/psbt.go | 9 +- .../wallet/btc-embedded/wallet.go | 198 ++++++++++-------- 4 files changed, 121 insertions(+), 102 deletions(-) diff --git a/common/fees.go b/common/fees.go index 9b93f50..0b4934c 100644 --- a/common/fees.go +++ b/common/fees.go @@ -21,9 +21,9 @@ var TreeTxSize = (&input.TxWeightEstimator{}). var CovenantTreeTxSize = TreeTxSize * 2 var ConnectorTxSize = (&input.TxWeightEstimator{}). - AddP2WKHInput(). - AddP2WKHOutput(). - AddP2WKHOutput(). + AddTaprootKeySpendInput(txscript.SigHashDefault). + AddP2TROutput(). + AddP2TROutput(). VSize() func ComputeForfeitMinRelayFee( diff --git a/server/cmd/arkd/commands.go b/server/cmd/arkd/commands.go index 1f29f45..96a1f34 100644 --- a/server/cmd/arkd/commands.go +++ b/server/cmd/arkd/commands.go @@ -77,6 +77,8 @@ var ( } ) +var timeout = time.Minute + func walletStatusAction(ctx *cli.Context) error { baseURL := ctx.String("url") tlsCertPath := ctx.String("tls-cert-path") @@ -220,7 +222,7 @@ func post[T any](url, body, key, macaroon, tlsCert string) (result T, err error) req.Header.Add("X-Macaroon", macaroon) } client := &http.Client{ - Timeout: 30 * time.Second, + Timeout: timeout, Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, @@ -267,7 +269,7 @@ func get[T any](url, key, macaroon, tlsCert string) (result T, err error) { } client := &http.Client{ - Timeout: 30 * time.Second, + Timeout: timeout, Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, @@ -331,7 +333,7 @@ func getBalance(url, macaroon, tlsCert string) (*balance, error) { req.Header.Add("X-Macaroon", macaroon) } client := &http.Client{ - Timeout: 30 * time.Second, + Timeout: timeout, Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, @@ -384,7 +386,7 @@ func getStatus(url, tlsCert string) (*status, error) { req.Header.Add("Content-Type", "application/json") client := &http.Client{ - Timeout: 30 * time.Second, + Timeout: timeout, Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, diff --git a/server/internal/infrastructure/wallet/btc-embedded/psbt.go b/server/internal/infrastructure/wallet/btc-embedded/psbt.go index 4ae685d..eab2a07 100644 --- a/server/internal/infrastructure/wallet/btc-embedded/psbt.go +++ b/server/internal/infrastructure/wallet/btc-embedded/psbt.go @@ -70,14 +70,11 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e } var managedAddress waddrmgr.ManagedPubKeyAddress - var isTaproot bool + isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript) - if len(in.TaprootLeafScript) > 0 && txscript.IsPayToTaproot(in.WitnessUtxo.PkScript) { - // segwit v1 - isTaproot = true - managedAddress = s.aspTaprootAddr + if len(in.TaprootLeafScript) > 0 { + managedAddress = s.aspKeyAddr } else { - // segwit v0 var err error managedAddress, _, _, err = s.wallet.ScriptForOutput(in.WitnessUtxo) if err != nil { diff --git a/server/internal/infrastructure/wallet/btc-embedded/wallet.go b/server/internal/infrastructure/wallet/btc-embedded/wallet.go index 9699d02..9d7c2fc 100644 --- a/server/internal/infrastructure/wallet/btc-embedded/wallet.go +++ b/server/internal/infrastructure/wallet/btc-embedded/wallet.go @@ -67,9 +67,15 @@ func (c WalletConfig) chainParams() *chaincfg.Params { type accountName string const ( - mainAccount accountName = "main" - connectorAccount accountName = "connector" - aspKeyAccount accountName = "aspkey" + // p2wkh scope + mainAccount accountName = "default" // default is always the first account (index 0) + + // p2tr scope + connectorAccount accountName = "default" + + // this account won't be restored by lnd, but it's not a problem cause it does not track any funds + // it's used to derive a constant public key to be used as "ASP key" in Vtxo scripts + aspKeyAccount accountName = "asp" // https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L23C8-L23C11 // biggest input size to compute the maximum dust amount @@ -102,7 +108,8 @@ type service struct { watchedScriptsLock sync.RWMutex watchedScripts map[string]struct{} - aspTaprootAddr waddrmgr.ManagedPubKeyAddress + // holds the data related to the ASP key used in Vtxo scripts + aspKeyAddr waddrmgr.ManagedPubKeyAddress } // WithNeutrino creates a start a neutrino node using the provided service datadir @@ -293,7 +300,7 @@ func (s *service) Create(_ context.Context, seed, password string) error { } func (s *service) Restore(_ context.Context, seed, password string) error { - return s.create(seed, password, 100) + return s.create(seed, password, 2000) // restore = create with a bigger recovery window } func (s *service) Unlock(_ context.Context, password string) error { @@ -304,8 +311,7 @@ func (s *service) Unlock(_ context.Context, password string) error { LogDir: s.cfg.Datadir, PrivatePass: pwd, PublicPass: pwd, - Birthday: time.Now(), - RecoveryWindow: 0, + RecoveryWindow: 512, NetParams: s.cfg.chainParams(), LoaderOptions: []btcwallet.LoaderOption{opt}, CoinSelectionStrategy: wallet.CoinSelectionLargest, @@ -364,7 +370,7 @@ func (s *service) Unlock(_ context.Context, password string) error { return fmt.Errorf("failed to cast address to managed pubkey address") } - s.aspTaprootAddr = managedPubkeyAddr + s.aspKeyAddr = managedPubkeyAddr break } } @@ -402,20 +408,30 @@ func (s *service) BroadcastTransaction(ctx context.Context, txHex string) (strin } func (s *service) ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error) { - amount, err := s.getBalance(connectorAccount) + utxos, err := s.listUtxos(p2trKeyScope) if err != nil { return 0, 0, err } + amount := uint64(0) + for _, utxo := range utxos { + amount += uint64(utxo.Output.Value) + } + return amount, 0, nil } func (s *service) MainAccountBalance(ctx context.Context) (uint64, uint64, error) { - amount, err := s.getBalance(mainAccount) + utxos, err := s.listUtxos(p2wpkhKeyScope) if err != nil { return 0, 0, err } + amount := uint64(0) + for _, utxo := range utxos { + amount += uint64(utxo.Output.Value) + } + return amount, 0, nil } @@ -423,7 +439,7 @@ func (s *service) DeriveAddresses(ctx context.Context, num int) ([]string, error addresses := make([]string, 0, num) for i := 0; i < num; i++ { - addr, err := s.deriveNextAddress(mainAccount) + addr, err := s.deriveNextAddress() if err != nil { return nil, err } @@ -439,7 +455,7 @@ func (s *service) DeriveAddresses(ctx context.Context, num int) ([]string, error } func (s *service) DeriveConnectorAddress(ctx context.Context) (string, error) { - addr, err := s.deriveNextAddress(connectorAccount) + addr, err := s.wallet.NewAddress(lnwallet.TaprootPubkey, false, string(connectorAccount)) if err != nil { return "", err } @@ -448,7 +464,7 @@ func (s *service) DeriveConnectorAddress(ctx context.Context) (string, error) { } func (s *service) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) { - return s.aspTaprootAddr.PubKey(), nil + return s.aspKeyAddr.PubKey(), nil } func (s *service) GetForfeitAddress(ctx context.Context) (string, error) { @@ -458,7 +474,7 @@ func (s *service) GetForfeitAddress(ctx context.Context) (string, error) { } if len(addrs) == 0 { - addr, err := s.deriveNextAddress(mainAccount) + addr, err := s.deriveNextAddress() if err != nil { return "", err } @@ -467,6 +483,10 @@ func (s *service) GetForfeitAddress(ctx context.Context) (string, error) { } for info, addrs := range addrs { + if info.KeyScope != p2wpkhKeyScope { + continue + } + if info.AccountName != string(mainAccount) { continue } @@ -500,15 +520,7 @@ func (s *service) ListConnectorUtxos(ctx context.Context, connectorAddress strin return nil, err } - connectorAccountNumber, err := w.AccountNumber(p2wpkhKeyScope, string(connectorAccount)) - if err != nil { - return nil, err - } - - utxos, err := w.UnspentOutputs(wallet.OutputSelectionPolicy{ - Account: connectorAccountNumber, - RequiredConfirmations: 0, - }) + utxos, err := s.listUtxos(p2trKeyScope) if err != nil { return nil, err } @@ -548,15 +560,7 @@ func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoi func (s *service) SelectUtxos(ctx context.Context, _ string, amount uint64) ([]ports.TxInput, uint64, error) { w := s.wallet.InternalWallet() - mainAccountNumber, err := w.AccountNumber(p2wpkhKeyScope, string(mainAccount)) - if err != nil { - return nil, 0, err - } - - utxos, err := w.UnspentOutputs(wallet.OutputSelectionPolicy{ - Account: mainAccountNumber, - RequiredConfirmations: 0, // allow uncomfirmed utxos - }) + utxos, err := s.listUtxos(p2wpkhKeyScope) if err != nil { return nil, 0, err } @@ -669,7 +673,6 @@ func (s *service) SignTransaction(ctx context.Context, partialTx string, extract ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes() continue } - } if err := psbt.Finalize(ptx, i); err != nil { @@ -1017,11 +1020,11 @@ func (s *service) create(mnemonic, password string, addrGap uint32) error { pwd := []byte(password) seed := bip39.NewSeed(mnemonic, password) opt := btcwallet.LoaderWithLocalWalletDB(s.cfg.Datadir, false, time.Minute) + config := btcwallet.Config{ LogDir: s.cfg.Datadir, PrivatePass: pwd, PublicPass: pwd, - Birthday: time.Now(), RecoveryWindow: addrGap, HdSeed: seed, NetParams: s.cfg.chainParams(), @@ -1036,12 +1039,19 @@ func (s *service) create(mnemonic, password string, addrGap uint32) error { return fmt.Errorf("failed to setup wallet loader: %s", err) } + if err := wallet.InternalWallet().Unlock([]byte(password), nil); err != nil { + return fmt.Errorf("failed to unlock wallet: %s", err) + } + + defer wallet.InternalWallet().Lock() + + if err := s.initAspKeyAccount(wallet); err != nil { + return err + } + if err := wallet.Start(); err != nil { return fmt.Errorf("failed to start wallet: %s", err) } - if err := s.initWallet(wallet); err != nil { - return err - } for { if !wallet.InternalWallet().ChainSynced() { @@ -1053,63 +1063,50 @@ func (s *service) create(mnemonic, password string, addrGap uint32) error { } log.Debugf("chain synced") - if addrGap > 0 { - // TODO: fix rescan - if err := wallet.InternalWallet().Rescan(nil, nil); err != nil { - return err - } + if err := s.initAspKeyAddress(wallet); err != nil { + return err } - wallet.InternalWallet().Lock() s.wallet = wallet return nil } -func (s *service) initWallet(wallet *btcwallet.BtcWallet) error { +// initAspKeyAccount creates the asp key account if it doesn't exist +func (s *service) initAspKeyAccount(wallet *btcwallet.BtcWallet) error { w := wallet.InternalWallet() - walletAccounts, err := w.Accounts(p2wpkhKeyScope) + p2trAccounts, err := w.Accounts(p2trKeyScope) if err != nil { return fmt.Errorf("failed to list wallet accounts: %s", err) } - var mainAccountNumber, connectorAccountNumber, aspKeyAccountNumber uint32 - if walletAccounts != nil { - for _, account := range walletAccounts.Accounts { - switch account.AccountName { - case string(mainAccount): - mainAccountNumber = account.AccountNumber - case string(connectorAccount): - connectorAccountNumber = account.AccountNumber - case string(aspKeyAccount): + + var aspKeyAccountNumber uint32 + + if p2trAccounts != nil { + for _, account := range p2trAccounts.Accounts { + if account.AccountName == string(aspKeyAccount) { aspKeyAccountNumber = account.AccountNumber - default: - continue + break } } } - if mainAccountNumber == 0 && connectorAccountNumber == 0 && aspKeyAccountNumber == 0 { - log.Debug("creating default accounts for ark wallet...") - mainAccountNumber, err = w.NextAccount(p2wpkhKeyScope, string(mainAccount)) - if err != nil { - return fmt.Errorf("failed to create %s: %s", mainAccount, err) - } - - connectorAccountNumber, err = w.NextAccount(p2wpkhKeyScope, string(connectorAccount)) - if err != nil { - return fmt.Errorf("failed to create %s: %s", connectorAccount, err) - } - + if aspKeyAccountNumber == 0 { + log.Debug("creating asp key account") aspKeyAccountNumber, err = w.NextAccount(p2trKeyScope, string(aspKeyAccount)) if err != nil { return fmt.Errorf("failed to create %s: %s", aspKeyAccount, err) } } - log.Debugf("main account number: %d", mainAccountNumber) - log.Debugf("connector account number: %d", connectorAccountNumber) - log.Debugf("asp key account number: %d", aspKeyAccountNumber) + log.Debugf("key account number: %d", aspKeyAccountNumber) + return nil +} + +// initAspKeyAddress generates the asp key address if it doesn't exist +// it also cache the address in s.aspKeyAddr field +func (s *service) initAspKeyAddress(wallet *btcwallet.BtcWallet) error { addrs, err := wallet.ListAddresses(string(aspKeyAccount), false) if err != nil { return err @@ -1131,7 +1128,7 @@ func (s *service) initWallet(wallet *btcwallet.BtcWallet) error { return fmt.Errorf("failed to cast address to managed pubkey address") } - s.aspTaprootAddr = managedAddr + s.aspKeyAddr = managedAddr } else { for info, addrs := range addrs { if info.AccountName != string(aspKeyAccount) { @@ -1161,7 +1158,7 @@ func (s *service) initWallet(wallet *btcwallet.BtcWallet) error { return fmt.Errorf("failed to cast address to managed pubkey address") } - s.aspTaprootAddr = managedPubkeyAddr + s.aspKeyAddr = managedPubkeyAddr break } } @@ -1171,26 +1168,12 @@ func (s *service) initWallet(wallet *btcwallet.BtcWallet) error { return nil } -func (s *service) getBalance(account accountName) (uint64, error) { - if !s.walletLoaded() { - return 0, ErrWalletNotLoaded - } - - balance, err := s.wallet.ConfirmedBalance(0, string(account)) - if err != nil { - return 0, err - } - - return uint64(balance), nil -} - -// this only supports deriving segwit v0 accounts -func (s *service) deriveNextAddress(account accountName) (btcutil.Address, error) { +func (s *service) deriveNextAddress() (btcutil.Address, error) { if !s.walletLoaded() { return nil, ErrWalletNotLoaded } - return s.wallet.NewAddress(lnwallet.WitnessPubKey, false, string(account)) + return s.wallet.NewAddress(lnwallet.WitnessPubKey, false, string(mainAccount)) } func (s *service) walletLoaded() bool { @@ -1211,6 +1194,43 @@ func (s *service) walletInitialized() bool { return exist } +func (s *service) listUtxos(scope waddrmgr.KeyScope) ([]*wallet.TransactionOutput, error) { + w := s.wallet.InternalWallet() + + accountNumber, err := w.AccountNumber(scope, string(mainAccount)) + if err != nil { + return nil, err + } + + utxos, err := w.UnspentOutputs(wallet.OutputSelectionPolicy{ + Account: accountNumber, + RequiredConfirmations: 0, + }) + if err != nil { + return nil, err + } + + filtered := make([]*wallet.TransactionOutput, 0, len(utxos)) + for _, utxo := range utxos { + scriptClass, _, _, err := txscript.ExtractPkScriptAddrs(utxo.Output.PkScript, w.ChainParams()) + if err != nil { + return nil, err + } + + switch scope { + case p2wpkhKeyScope: + if scriptClass == txscript.WitnessV0PubKeyHashTy { + filtered = append(filtered, utxo) + } + case p2trKeyScope: + if scriptClass == txscript.WitnessV1TaprootTy { + filtered = append(filtered, utxo) + } + } + } + return filtered, nil +} + func withChainSource(chainSource chain.Interface) WalletOption { return func(s *service) error { if s.chainSource != nil {