account: implement client side recovery process

This commit is contained in:
positiveblue 2022-01-10 16:22:32 -08:00
parent baf2042a0b
commit ac0a554d7d
No known key found for this signature in database
GPG key ID: 4FFF2510928804DC
2 changed files with 279 additions and 10 deletions

View file

@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/pool/poolscript"
"github.com/lightningnetwork/lnd/keychain"
@ -34,6 +35,12 @@ const (
// batch key in every environment.
InitialBatchKey = "02824d0cbac65e01712124c50ff2cc74ce22851d7b444c1bf2" +
"ae66afefb8eaf27f"
// maxBatchCounter is the maximum number of batches that we consider
// worth checking.
//
// NOTE: currently there are about 1100 batches (Jan 2022) on mainnet
maxBatchCounter = 5000
)
// GetAuctioneerData returns the auctioneer data for a given environment.
@ -175,11 +182,26 @@ func getBitcoinConn(cfg *BitcoinConfig) (*rpcclient.Client, error) {
return rpcclient.New(connCfg, nil)
}
// getBlockTxs returns all the transactions in the block for the given height.
func getBlockTxs(client *rpcclient.Client, height int64) ([]*wire.MsgTx, error) {
blockHash, err := client.GetBlockHash(height)
if err != nil {
return nil, err
}
block, err := client.GetBlock(blockHash)
if err != nil {
return nil, err
}
return block.Transactions, nil
}
// RecoverAccounts tries to recover valid accounts using the given configuration.
func RecoverAccounts(ctx context.Context, cfg RecoveryConfig) ([]*Account,
error) {
client, err := getBitcoinConn(&BitcoinConfig{})
client, err := getBitcoinConn(cfg.BitcoinConfig)
if err != nil {
return nil, err
}
@ -197,7 +219,6 @@ func RecoverAccounts(ctx context.Context, cfg RecoveryConfig) ([]*Account,
if err != nil {
return nil, err
}
// Are we shutting down?
select {
case <-cfg.Quit:
@ -215,9 +236,161 @@ func recoverInitialState(ctx context.Context, cfg RecoveryConfig) ([]*Account,
log.Debugf("Recovering initial states for %d accounts...",
cfg.AccountTarget)
// TODO (positiveblue): recover initial state
possibleAccounts, err := recreatePossibleAccounts(ctx, cfg)
if err != nil {
return nil, err
}
return nil, nil
accounts := findAccounts(cfg, possibleAccounts)
log.Debugf("Found initial tx for %d/%d accounts", len(accounts),
cfg.AccountTarget)
return accounts, nil
}
// recreatePossibleAccounts returns a set of potentially valid accounts
// by generating a set of recovery keys.
func recreatePossibleAccounts(ctx context.Context,
cfg RecoveryConfig) ([]*Account, error) {
possibleAccounts := make([]*Account, 0, cfg.AccountTarget)
// Prepare the keys we are going to try. Possibly not all of them will
// be used.
KeyDescriptors, err := GenerateRecoveryKeys(
ctx, cfg.AccountTarget, cfg.Wallet,
)
if err != nil {
return nil, fmt.Errorf("error generating keys: %v", err)
}
for _, keyDes := range KeyDescriptors {
secret, err := cfg.Signer.DeriveSharedKey(
ctx, cfg.AuctioneerPubKey, &keyDes.KeyLocator,
)
if err != nil {
return nil, fmt.Errorf("error deriving shared key: %v",
err)
}
acc := &Account{
TraderKey: keyDes,
AuctioneerKey: cfg.AuctioneerPubKey,
Secret: secret,
State: StateOpen,
}
possibleAccounts = append(possibleAccounts, acc)
}
return possibleAccounts, nil
}
// findAccounts tries to find on-chain footprint for the creation of a set
// of possible pool accounts.
func findAccounts(cfg RecoveryConfig, possibleAccounts []*Account) []*Account {
// The process looks like:
// - Fix the batch counter.
// - Fix a block height.
// - Recreate the script for each of the possible accounts.
// - Look for the script in all of the node txs.
target := cfg.AccountTarget
accounts := make([]*Account, 0, target)
// Once we find an account, we remove it from the target list to avoid
// finding later account modification transactions for an account we
// already found an initial TX for. We'll walk the chain of updates in
// a secondary step. We use the account index as the map key since that
// is unique across accounts.
remainingAccounts := make(map[uint32]*Account, len(possibleAccounts))
for _, possibleAccount := range possibleAccounts {
remainingAccounts[possibleAccount.TraderKey.Index] = possibleAccount
}
helper := &poolscript.RecoveryHelper{
BatchKey: cfg.InitialBatchKey,
AuctioneerKey: cfg.AuctioneerPubKey,
}
for batchCounter := 0; batchCounter < maxBatchCounter; batchCounter++ {
batchKey := helper.BatchKey
accountLoop:
for acctIdx, acc := range remainingAccounts {
acc := acc
traderKey := acc.TraderKey.PubKey
helper.NextAccount(traderKey, acc.Secret)
for h := cfg.FirstBlock; h < cfg.LastBlock; h++ {
// Are we shutting down?
select {
case <-cfg.Quit:
return nil
default:
}
tx, idx, ok, err := helper.LocateAnyOutput(
h, cfg.Transactions,
)
if err != nil {
log.Debugf("Unable to generate script "+
"height=%v, batch_key=%x, "+
"trader_key=%x): %v", h,
batchKey.SerializeCompressed(),
traderKey.SerializeCompressed(),
err)
// Go to next account.
continue accountLoop
}
// If it's NOT a match, keep trying.
if !ok {
continue
}
log.Debugf("Found initial account state for "+
"account %x",
traderKey.SerializeCompressed())
// If it's a match, populate the account
// information.
acc.Expiry = h
acc.Value = btcutil.Amount(
tx.TxOut[idx].Value,
)
acc.BatchKey = batchKey
acc.OutPoint = wire.OutPoint{
Hash: tx.TxHash(),
Index: idx,
}
acc.LatestTx = tx
acc.State = StateOpen
// Remove the current account, so we don't try
// to find it again.
delete(remainingAccounts, acctIdx)
accounts = append(accounts, acc)
// If we already found all the accounts that
// we were looking for quit the search.
if len(accounts) == int(target) {
return accounts
}
}
}
helper.NextBatchKey()
if batchCounter > 0 && batchCounter%100 == 0 {
log.Debugf("Tried looking for accounts in %d batches, "+
"will try up to %d", batchCounter,
maxBatchCounter)
}
}
return accounts
}
// updateAccountStates tries to update the states for every provided
@ -226,16 +399,112 @@ func recoverInitialState(ctx context.Context, cfg RecoveryConfig) ([]*Account,
func updateAccountStates(cfg RecoveryConfig,
accounts []*Account) ([]*Account, error) {
// TODO (positiveblue): update account states
for i := cfg.FirstBlock; i < uint32(cfg.CurrentBlockHeight); i++ {
blockHeight := i
return nil, nil
txs, err := getBlockTxs(cfg.bitcoinClient, int64(blockHeight))
if err != nil {
return nil, err
}
for idx, acc := range accounts {
// It's safe to ignore accounts that are in a
// terminal state.
if acc.State == StateClosed {
continue
}
tx, found := poolscript.MatchPreviousOutPoint(
acc.OutPoint, txs,
)
if !found {
// Go to next account.
continue
}
newAcc, err := findAccountUpdate(cfg, acc, tx)
if err != nil {
// If we cannot find the account update
// we assume it was closed/spent.
acc.State = StateClosed
acc.HeightHint = blockHeight
log.Debugf("Account was spent in tx %x but "+
"not re-created. Assuming account was "+
"fully spent or closed", tx.TxHash())
continue
}
newAcc.HeightHint = blockHeight
accounts[idx] = newAcc
}
}
return accounts, nil
}
// findAccountUpdate tries to find the new account values after an update.
func findAccountUpdate(cfg RecoveryConfig, acc *Account,
tx *wire.MsgTx) (*Account, error) {
newAcc := acc.Copy()
newAcc.BatchKey = poolscript.IncrementKey(newAcc.BatchKey)
helper := &poolscript.RecoveryHelper{
BatchKey: newAcc.BatchKey,
AuctioneerKey: cfg.AuctioneerPubKey,
}
helper.NextAccount(acc.TraderKey.PubKey, acc.Secret)
// We only have a tx so if there is a match we know what tx it is.
_, idx, ok, err := helper.LocateAnyOutput(
newAcc.Expiry, []*wire.MsgTx{tx},
)
if err != nil {
return nil, fmt.Errorf("account update not found")
}
if ok {
newAcc.Value = btcutil.Amount(tx.TxOut[idx].Value)
newAcc.OutPoint = wire.OutPoint{
Hash: tx.TxHash(),
Index: idx,
}
newAcc.LatestTx = tx
return newAcc, nil
}
// If the update included a new expiration date we need to brute force
// our new expiration date again.
for height := cfg.FirstBlock; height <= cfg.LastBlock; height++ {
_, idx, ok, err := helper.LocateAnyOutput(
height, []*wire.MsgTx{tx},
)
if err != nil {
return nil, fmt.Errorf("account update not found")
}
if !ok {
continue
}
newAcc.Expiry = height
newAcc.Value = btcutil.Amount(tx.TxOut[idx].Value)
newAcc.OutPoint = wire.OutPoint{
Hash: tx.TxHash(),
Index: idx,
}
newAcc.LatestTx = tx
return newAcc, nil
}
return nil, fmt.Errorf("account update not found")
}
// GenerateRecoveryKeys generates a list of key descriptors for all possible
// keys that could be used for trader accounts, up to a hard coHashded limit.
// keys that could be used for trader accounts, up to a hard coded limit.
func GenerateRecoveryKeys(ctx context.Context, accountTarget uint32,
wallet lndclient.WalletKitClient) (
[]*keychain.KeyDescriptor, error) {
wallet lndclient.WalletKitClient) ([]*keychain.KeyDescriptor, error) {
acctKeys := make([]*keychain.KeyDescriptor, accountTarget)
for i := uint32(0); i < accountTarget; i++ {

View file

@ -321,7 +321,7 @@ func LocateOutputScript(tx *wire.MsgTx, script []byte) (uint32, bool) {
}
// MatchPreviousOutPoint determines whether or not a PreviousOutPoint appears
// in anye of the provided transactions.
// in any of the provided transactions.
func MatchPreviousOutPoint(op wire.OutPoint, txs []*wire.MsgTx) (*wire.MsgTx,
bool) {