Skip to content

Commit

Permalink
Find wallet for a redemption (#630)
Browse files Browse the repository at this point in the history
Add `findWalletForRedemption` function that returns the wallet details
needed to request a redemption based on the redemption amount. This
function looks for the oldest active wallet(wallet must be in `Live`
state) that has enough BTC to handle a redemption request.
  • Loading branch information
lukasz-zimnoch committed Jul 10, 2023
2 parents f5f300b + e68a7ef commit f53cb9c
Show file tree
Hide file tree
Showing 8 changed files with 746 additions and 9 deletions.
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ e032bba7ce52a877a77573fb5a14944623c77d02
7ad277ff8369b201515ba22872c1251ec93a6b81
5c7e2e3620ec06fb9a7c358c541d491da977ec08
3d8c4861ac467f0390733916775a9ccfafe752e3
921349ff07db0effe9bc658afab6961ff262f35f

# s/btc/BTC
da039720b6eb36e5f7102e83a3e2bb95a09b5772
16 changes: 16 additions & 0 deletions typescript/src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
DkgResultChallengedEvent,
DkgResultSubmittedEvent,
NewWalletRegisteredEvent,
Wallet,
} from "./wallet"
import type { ExecutionLoggerFn } from "./backoff"

Expand Down Expand Up @@ -244,6 +245,21 @@ export interface Bridge {
* Returns the attached WalletRegistry instance.
*/
walletRegistry(): Promise<WalletRegistry>

/**
* Gets details about a registered wallet.
* @param walletPublicKeyHash The 20-byte wallet public key hash (computed
* using Bitcoin HASH160 over the compressed ECDSA public key).
* @returns Promise with the wallet details.
*/
wallets(walletPublicKeyHash: Hex): Promise<Wallet>

/**
* Builds the UTXO hash based on the UTXO components.
* @param utxo UTXO components.
* @returns The hash of the UTXO.
*/
buildUtxoHash(utxo: UnspentTransactionOutput): Hex
}

/**
Expand Down
82 changes: 75 additions & 7 deletions typescript/src/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type {
Bridge as ContractBridge,
Deposit as ContractDeposit,
Redemption as ContractRedemption,
Wallets,
} from "../typechain/Bridge"
import type { WalletRegistry as ContractWalletRegistry } from "../typechain/WalletRegistry"
import type { TBTCVault as ContractTBTCVault } from "../typechain/TBTCVault"
Expand All @@ -57,6 +58,8 @@ import {
DkgResultChallengedEvent,
DkgResultSubmittedEvent,
NewWalletRegisteredEvent,
Wallet,
WalletState,
} from "./wallet"

type ContractDepositRequest = ContractDeposit.DepositRequestStructOutput
Expand Down Expand Up @@ -639,18 +642,20 @@ export class Bridge
return undefined
}

const { ecdsaWalletID } = await backoffRetrier<{ ecdsaWalletID: string }>(
this._totalRetryAttempts
)(async () => {
return await this._instance.wallets(activeWalletPublicKeyHash)
})
const { walletPublicKey } = await this.wallets(
Hex.from(activeWalletPublicKeyHash)
)

return walletPublicKey.toString()
}

private async getWalletCompressedPublicKey(ecdsaWalletID: Hex): Promise<Hex> {
const walletRegistry = await this.walletRegistry()
const uncompressedPublicKey = await walletRegistry.getWalletPublicKey(
Hex.from(ecdsaWalletID)
ecdsaWalletID
)

return compressPublicKey(uncompressedPublicKey)
return Hex.from(compressPublicKey(uncompressedPublicKey))
}

// eslint-disable-next-line valid-jsdoc
Expand Down Expand Up @@ -694,6 +699,69 @@ export class Bridge
signerOrProvider: this._instance.signer || this._instance.provider,
})
}

// eslint-disable-next-line valid-jsdoc
/**
* @see {ChainBridge#wallets}
*/
async wallets(walletPublicKeyHash: Hex): Promise<Wallet> {
const wallet = await backoffRetrier<Wallets.WalletStructOutput>(
this._totalRetryAttempts
)(async () => {
return await this._instance.wallets(
walletPublicKeyHash.toPrefixedString()
)
})

return this.parseWalletDetails(wallet)
}

/**
* Parses a wallet data using data fetched from the on-chain contract.
* @param wallet Data of the wallet.
* @returns Parsed wallet data.
*/
private async parseWalletDetails(
wallet: Wallets.WalletStructOutput
): Promise<Wallet> {
const ecdsaWalletID = Hex.from(wallet.ecdsaWalletID)

return {
ecdsaWalletID,
walletPublicKey: await this.getWalletCompressedPublicKey(ecdsaWalletID),
mainUtxoHash: Hex.from(wallet.mainUtxoHash),
pendingRedemptionsValue: wallet.pendingRedemptionsValue,
createdAt: wallet.createdAt,
movingFundsRequestedAt: wallet.movingFundsRequestedAt,
closingStartedAt: wallet.closingStartedAt,
pendingMovedFundsSweepRequestsCount:
wallet.pendingMovedFundsSweepRequestsCount,
state: WalletState.parse(wallet.state),
movingFundsTargetWalletsCommitmentHash: Hex.from(
wallet.movingFundsTargetWalletsCommitmentHash
),
}
}

// eslint-disable-next-line valid-jsdoc
/**
* Builds the UTXO hash based on the UTXO components. UTXO hash is computed as
* `keccak256(txHash | txOutputIndex | txOutputValue)`.
*
* @see {ChainBridge#buildUtxoHash}
*/
buildUtxoHash(utxo: UnspentTransactionOutput): Hex {
return Hex.from(
utils.solidityKeccak256(
["bytes32", "uint32", "uint64"],
[
utxo.transactionHash.reverse().toPrefixedString(),
utxo.outputIndex,
utxo.value,
]
)
)
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
requestRedemption,
submitRedemptionProof,
getRedemptionRequest,
findWalletForRedemption,
} from "./redemption"

import {
Expand All @@ -31,6 +32,7 @@ export const TBTC = {
getRevealedDeposit,
requestRedemption,
getRedemptionRequest,
findWalletForRedemption,
}

export const SpvMaintainer = {
Expand Down
141 changes: 141 additions & 0 deletions typescript/src/redemption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import {
UnspentTransactionOutput,
Client as BitcoinClient,
TransactionHash,
encodeToBitcoinAddress,
} from "./bitcoin"
import { Bridge, Identifier, TBTCToken } from "./chain"
import { assembleTransactionProof } from "./proof"
import { WalletState } from "./wallet"
import { BitcoinNetwork } from "./bitcoin-network"
import { Hex } from "./hex"

/**
Expand Down Expand Up @@ -408,3 +411,141 @@ export async function getRedemptionRequest(

return redemptionRequests[0]
}

/**
* Finds the oldest active wallet that has enough BTC to handle a redemption request.
* @param amount The amount to be redeemed in satoshis.
* @param redeemerOutputScript The redeemer output script the redeemed funds
* are supposed to be locked on. Must be un-prefixed and not prepended
* with length.
* @param bitcoinNetwork Bitcoin network.
* @param bridge The handle to the Bridge on-chain contract.
* @param bitcoinClient Bitcoin client used to interact with the network.
* @returns Promise with the wallet details needed to request a redemption.
*/
export async function findWalletForRedemption(
amount: BigNumber,
redeemerOutputScript: string,
bitcoinNetwork: BitcoinNetwork,
bridge: Bridge,
bitcoinClient: BitcoinClient
): Promise<{
walletPublicKey: string
mainUtxo: UnspentTransactionOutput
}> {
const wallets = await bridge.getNewWalletRegisteredEvents()

let walletData:
| {
walletPublicKey: string
mainUtxo: UnspentTransactionOutput
}
| undefined = undefined
let maxAmount = BigNumber.from(0)

for (const wallet of wallets) {
const { walletPublicKeyHash } = wallet
const { state, mainUtxoHash, walletPublicKey, pendingRedemptionsValue } =
await bridge.wallets(walletPublicKeyHash)

// Wallet must be in Live state.
if (state !== WalletState.Live) {
console.debug(
`Wallet is not in Live state ` +
`(wallet public key hash: ${walletPublicKeyHash.toString()}). ` +
`Continue the loop execution to the next wallet...`
)
continue
}

if (
mainUtxoHash.equals(
Hex.from(
"0x0000000000000000000000000000000000000000000000000000000000000000"
)
)
) {
console.debug(
`Main utxo not set for wallet public ` +
`key hash(${walletPublicKeyHash.toString()}). ` +
`Continue the loop execution to the next wallet...`
)
}

const pendingRedemption = await bridge.pendingRedemptions(
walletPublicKey.toString(),
redeemerOutputScript
)

if (pendingRedemption.requestedAt != 0) {
console.debug(
`There is a pending redemption request from this wallet to the ` +
`same Bitcoin address. Given wallet public key hash` +
`(${walletPublicKeyHash.toString()}) and redeemer output script ` +
`(${redeemerOutputScript}) pair can be used for only one ` +
`pending request at the same time. ` +
`Continue the loop execution to the next wallet...`
)
continue
}

const walletBitcoinAddress = encodeToBitcoinAddress(
wallet.walletPublicKeyHash.toString(),
true,
bitcoinNetwork
)

// TODO: In case a wallet is working on something (e.g. redemption) and a
// Bitcoin transaction was already submitted by the wallet to the bitcoin
// chain (new utxo returned from bitcoin client), but proof hasn't been
// submitted yet to the Bridge (old main utxo returned from the Bridge) the
// `findWalletForRedemption` function will not find such a wallet. To cover
// this case, we should take, for example, the last 5 transactions made by
// the wallet into account. We will address this issue in a follow-up work.
const utxos = await bitcoinClient.findAllUnspentTransactionOutputs(
walletBitcoinAddress
)

// We need to find correct utxo- utxo components must point to the recent
// main UTXO of the given wallet, as currently known on the chain.
const mainUtxo = utxos.find((utxo) =>
mainUtxoHash.equals(bridge.buildUtxoHash(utxo))
)

if (!mainUtxo) {
console.debug(
`Could not find matching UTXO on chains ` +
`for wallet public key hash(${walletPublicKeyHash.toString()}). ` +
`Continue the loop execution to the next wallet...`
)
continue
}

const walletBTCBalance = mainUtxo.value.sub(pendingRedemptionsValue)

// Save the max possible redemption amount.
maxAmount = walletBTCBalance.gt(maxAmount) ? walletBTCBalance : maxAmount

if (walletBTCBalance.gte(amount)) {
walletData = {
walletPublicKey: walletPublicKey.toString(),
mainUtxo,
}

break
}

console.debug(
`The wallet (${walletPublicKeyHash.toString()})` +
`cannot handle the redemption request. ` +
`Continue the loop execution to the next wallet...`
)
}

if (!walletData)
throw new Error(
`Could not find a wallet with enough funds. Maximum redemption amount is ${maxAmount} Satoshi.`
)

return walletData
}
Loading

0 comments on commit f53cb9c

Please sign in to comment.