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>
This commit is contained in:
Louis Singer
2024-09-27 17:47:42 +02:00
committed by GitHub
parent ab6ae36eb5
commit 72e31d839a
4 changed files with 121 additions and 102 deletions

View File

@@ -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(

View File

@@ -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,
},

View File

@@ -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 {

View File

@@ -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 {