Stakers periodically create 51-of-100 ecdsa-backed wallets to hold frozen BTC assets to maintain account balances. Depositors send BTC funds to the most-recently-created-wallet by using pay-to-script-hash (P2SH) which contains hashed information about the depositor’s minting ethereum address. Once the block is mined, the depositor reveals their desired ethereum minting address to the ethereum chain. The bridge listens for these sorts of messages and when it gets one, it checks the bitcoin network to make sure the funds line up. If everything checks out, then we update your ethereum-side account balance.
A user with an account balance supplies a bitcoin address. The system decreases their account balance and releases the equivalent amount of bitcoin to the user from the redeeming bitcoin wallet.
The redeeming wallet will be determined by the system on chain via an upgradable (by governance) contract. The initial implementation for this contract should select the oldest wallet. Redemptions should be batched (details to be determined) to both decrease fees and processing time.
One possible implementation for efficient on-chain calculation of the oldest wallet is to store the wallet information in a tree similar to what we use for sortition pools, ensuring that no matter how many wallets we have active, the cost of finding a wallet is always logarithmic over the maximum number of wallets during the system’s lifetime.
The maximum redemption size is capped by the size of the largest wallet, and any redemption bigger than that will need to be split into multiple redemptions.
The system is able to collect fees in the form of tBTC when the BTC is deposited and when it is redeemed. Rather than paying operators at those two instances we would prefer to pay them continuously. Since we do not have the tBTC to pay them with until we collect the redemption fee (and can’t mint it and maintain the peg), we must turn to another token. Instead, the system pays operators in T tokens from a treasury funded by the T-token DAO and whenever it collects tBTC fees it uses those fees to buy T tokens back for its treasury.
Governable Parameters:
-
heartbeat
: The number of group members required for a heartbeat to be successful.
Non-Governable Parameters:
-
threshold
: The number of signers that can be controlled by the adversary before the key is in danger. -
dkg
: The minimum number of members we’re allowed to drop down to during the DKG group formation re-try period. -
group_size
: The total size of the signing group.
threshold < heartbeat < dkg < group_size
Note: threshold
, dkg
and group_size
are mission-critical, cornerstone
parameters for the system, and will be hardcoded into the bridge contract. If
we want to change these parameters, we will need to upgrade the bridge
contract.
The total gas cost for sortition increases linearly with group size by at least
20k gas per member (though, we think we can get this down to roughly 6k gas per
member in the happy path with some upcoming optimizations). Requiring more
signers to sign messages makes it harder for adversaries to take over the
group, but makes it so that the pool is less resilient to undelegation or
operators going offline. Raising heartbeat
decreases the chance that you’ll
not have enough signers to sign transactions, but decreases a wallets lifespan
and increases overhead. Lowering dkg
increases the chance that we’re able to
create a wallet successfully but gives us a wallet with potentially fewer
operators and a lower lifespan.
Also important is optics - even though a group_size
of 70 with a threshold
of 40 is equally secure against being controlled by an adversary as a
group_size
of 100 with a threshold
of 50, it might not feel as secure.
That perception may cause a decreased willingness to invest capital in the
system leading to less money going over the bridge, even though the fees are
less overall.
Since we don’t have any workable data that allows us to reasonably estimate
costs (especially with regard to optics), I suggest we start the system out
at decent schelling
point of group_size = 100
, threshold = 50
, heartbeat = 70
, dkg = 90
and then let
governance adjust from there once we’ve gathered data from the system being used.
See RFC 2: tBTCv2 Group Selection and Key Generation for a deeper dive here
The bridge is able to maintain a clean separation of concerns as well as provide the backbone an extensible financial system rooted in bitcoin-on-ethereum by concerning itself just with how much bitcoin has gone over the bridge. We keep track of account balances like:
contract Balances {
mapping(address => uint256) private unsweptBalances;
mapping(address => uint256) private balances;
}
When bitcoin enters the system, the associated account’s unsweptBalance increases. When that deposit is swept, that amount is transferred to the balance. If a user decides to mint a TBTC bitcoin from their balance, their balance would be drained and a TBTC token would be minted. When a user brings back TBTC to the system, the token is burned and the account balance increases. If the user wants BTC, they can drain their account balance to redeem it.
This more abstract design lets us not only do things like mint TBTC by draining account balances, but also move into other financial concepts like bitcoin-collateralized loans or bitcoin-backed stablecoins.
Neither of the above concepts are in-scope for this RFC, but the important part is that we want to make sure we’re starting with the more flexible account-balances design so that we’re not stuck later.
Once we know the active wallet’s public key hash, the dApp can put together a pay-to-script-hash (P2SH) address to receive the funds. This script will be unique to each depositor and will look like:
<eth-address> DROP
<blinding-factor> DROP
DUP HASH160 <signingGroupPubkeyHash> EQUAL
IF
CHECKSIG
ELSE
DUP HASH160 <refundPubkeyHash> EQUALVERIFY
<locktime> CHECKLOCKTIMEVERIFY DROP
CHECKSIG
ENDIF
Since each depositor has their own ethereum address and their own blinding factor, each depositor’s script will be unique, and the hash of each depositor’s script will be unique.
In order to unlock the funds, one must provide the unhashed script, (which
means that they know the eth address and blinding factor), as well as an
unlocking script with a signature and public key. If the sig+pubkey matches the
signing group public key, the funds are able to be moved immediately. If the
sig+pubkey matches the refund public key, then the funds can be moved after 30
days (specified as locktime
).
Governable Parameters:
-
sweep_period
: The amount of time we wait between scheduled sweeps on a wallet.
After the deposit transaction has been mined, the user is able to reveal their ethereum address and blinding factor to the ethereum chain. The bridge listens for these sorts of messages and when it sees one, is able to generate a script that can spend the funds. Once successful, we increase the account’s unswept balance and charge a deposit fee.
Additionally and optionally, as a part of the reveal transaction, the user the declare that they want their swept funds to be immediately minted into TBTC. This saves the user from having to make separate transactions or wait for a sweep to occur before an additional transaction.
Second, we schedule an operation that batches all outstanding known-refundable
transactions together to be combined with the existing wallet output into a
single output. The frequency of this operation is the sweep_period
. When this
sweep occurs, we decrease the relevant accounts' unswept balances
and increase their balances. This disables any outstanding 30-day refunds.
A bitcoin transaction is an amount and a script. The script can be something as simple as "these funds can be spent by wallet 0xabc", or in our case, as complex as "these funds can be spent by wallet 0xabc but if they aren’t spent within 30 days they can be spent by wallet 0x123". This gives us the ability to create deposits that automatically are refunded after 30 days if they aren’t swept. Thus, if a user misfunds or they get cold feet (for any reason), all they need to do is not submit their reveal and wait 30 days.
Governable Parameters:
-
sweeping_refund_safety_time
: The amount of time prior to when a UTXO becomes eligible for a refund where we will not include it in a sweeping transaction. -
sweep_period
: The amount of time we wait between scheduled sweeps on a wallet. -
sweep_max_deposits
: The number of non-dust unswept revealed bitcoin deposits that will trigger an early sweep on a wallet. -
dust_threshold
: The minimum bitcoin deposit amount for the transaction to be considered for a sweep. -
base_btc_fee_max
: The highest amount of BTC that operators can initially propose as a fee for miners in a sweeping transaction. -
sweeping_fee_bump_period
: The amount of time we wait to see if a sweeping transaction is mined before increasing the fee. -
sweeping_fee_multiplier_increment
: The amount we add to the sweeping fee multiplier each time a sweeping transaction is not mined within thesweeping_fee_bump_period
. For example, if this param is set to 0.2 and we are currently at 1.6x, then the next time we would try 1.8x. -
sweeping_fee_max_multiplier
: The highest we will try to increment the fee multiplier to before giving up and picking a new base fee and different deposits to sweep. -
btc_fee_max
: The highest amount of BTC that operators can eventually propose as a fee for miners for sweeping transaction.
The operators sign a transaction that unlocks all of the revealed deposits
above the dust_threshold
, combines them into a single UTXO with the existing
UTXO, and relocks that transactions without a 30-day refund clause to same
wallet. This has two main effects: it consolidates the UTXO set and it
disables the refund.
Caveat: We only include deposits in batches that have at least
sweeping_refund_safety_time
their refund window. This prevents potential
attacks or corner cases where we create a transaction with a valid, unspent
input, but by the time we have signed that transaction, the depositor has
already submitted a refund to the mining pool. Giving ourselves this leeway
stops that from happening. Once a deposit crosses that
sweeping_refund_safety_time
threshold, the depositor should wait and then
refund their deposit.
Caveat: A wallet only sweeps deposits that were deposited while while the wallet was either the youngest or second-youngest wallet. The dApp will only point deposits to the youngest wallet, so any other wallet receiving deposits is the result of funky custom user behavior. In those cases, the users will need to wait 30 days for their refund.
This process is called a "sweep", and occurs after sweep_period
has passed or
if enough deposits have accumulated according to sweep_max_deposits
, whichever
comes first. Any deposit below dust_threshold
is ignored, both for triggering
a sweep as well as being included in a sweep.
The sweeping transaction will cost some amount of bitcoin based on what miners
are charging for the bitcoin fee in the current market conditions. The fee is
split in proportion to the number of UTXOs associated to each depositor. Once
the transaction is submitted to the bitcoin mempool, the miners will either
include it in a block within sweeping_fee_bump_period
or not. If they don’t,
then we increment a fee multiplier: fee_multiplier = fee_multiplier
and then calculate the new fee:
sweeping_fee_multiplier_incrementfee =
base_fee * fee_multiplier
. We repeat until either the transaction posts or
sweeping_fee_multiplier_increment
exceeds sweeping_fee_max_multiplier
.
Note: We do not allow users to specify a max btc fee. When users deposit,
they’re agreeing to be swept at whatever fee the operators decide is
appropriate (based on https://blockstream.info/api/fee-estimates). Operators
cannot pick a starting fee higher than base_btc_fee_max
and they can never
choose a fee higher than btc_fee_max
.
When the transaction clears, and the information has made its way over the relay maintainer, then another transaction needs to be created to on the ethereum side to update the account balances. The users unswept balances are decreased, and their swept balances are increased (after paying their share of the bitcoin sweeping fee).
This transaction will be expensive gas-wise, and can be submitted by anyone with the motivation to do so. For more details on transaction incentives, check out the dedicated section.
Caveat: The sweeping_fee_bump_period
and sweeping_fee_max_multiplier
parameters should be constrained such that one sweep should either finish and
either post or fail before the next sweep is scheduled (via sweep_period
) to
start. This is because sweeps include the main UTXO as one of the inputs, which
is the result of the previous sweep’s output.
The main downside to this approach is that it can take, in the worst case, up
to sweep_period
for a user to be able to mint TBTC. To help
alleviate this, two suggestions:
1) We surface when the next scheduled sweep and the accumulation threshold data is somewhere in the dApp. This allows users to feel a lot better about when sweeps are happening, and feel better about when their funds will be available. There is also something to be said about the marketing around explaining that we’re batching in order to reduce fees across the board for the end user, which allows for the decentralized product to compete with the centralized ones.
2) We allow users to request that their TBTC is minted as soon as they have a swept account balance. This makes it so they don’t have to wait, check, and come back later and mint.
Combining these ideas, a user would deposit some BTC, reveal their eth address and blinding factor, and then request that TBTC gets minted ASAP. Checking the dApp, they can see that they should expect TBTC in their provided wallet address in 3 hours with no further interaction.
Governable Parameters:
-
sweep_period
: The amount of time we wait between scheduled sweeps on a wallet. -
sweep_max_deposits
: The number of non-dust unswept revealed bitcoin deposits that will trigger an early sweep on a wallet.
We’ve established in the sweeping section that we should sweep
whenever enough time has passed to exceed the sweep_period
or whenever enough
deposits are in the queue according to sweep_max_deposits
. If we sweep early
because a lot of deposits have been revealed, then we don’t "push back" our scheduled
sweep_period
sweep. Rather, that sweep continues as planned, and if there
are no deposits with sweeping fee high enough to be
included in a sweep (maybe because they all got swept in the sweep_max_deposits
sweep), then we wait until the next sweep and repeat the process.
Example: We have a sweep_period = 8 hours
and sweep_max_deposits = 10
. At
13:00, a sweep just occurred, and the next is scheduled for 21:00. At 15:00, 13
deposits gets revealed which triggers a sweep due to sweep_max_deposits
. Rather
than pushing back the next scheduled sweep to 23:00, it remains at 21:00. If by
21:00 there are any deposits with a high enough sweeping fee
to be included in a sweep, we do it. Otherwise, we schedule the next sweep for
05:00 the next day. The process repeats.
Here, we’re making the tradeoff between reducing fees (having less frequent batches) and increasing reliability from a user experience standpoint.
Governable Parameters:
-
redemption_request_timeout_redeemer_bonus_multiplier
: The percentage of the notifier reward from the staking contract the redeemer receives in case of a redemption timeout. -
redemption_request_timeout_slashing_amount
: The amount of stake slashed from each member of a wallet for a timed-out redemption request. -
redemption_request_timeout
: The amount of time the wallet has to provide redemption proof. -
redemption_treasury_fee
: The percentage of redeemed amount put aside as a treasury fee. -
wallet_min_closure_btc
: The smallest amount of BTC a wallet can hold before we attempt to close the wallet and transfer the funds to a randomly selected wallet.
To initiate a redemption, a user with a swept balance > x
supplies a bitcoin
address. Then, the system calculates the redemption fee redemption_treasury_fee
,
and releases an amount of bitcoin y
such that x = y + redemption_treasury_fee
to the supplied bitcoin address. The remaining redemption_treasury_fee
is sold
by the system to buy back T
tokens (more about this
process in the fee section) to pay to the operators.
In the MVP version of the system, a redemption is capped at the amount of
bitcoin contained in the largest wallet. The wallet doing the redemption is
selected by the redeemer, but the dApp should suggest that this is the oldest
wallet that contains enough bitcoin to fulfil the redemption. If more BTC needs
to be redeemed than there is in the largest wallet, then the user needs to
submit multiple redemptions. After a redemption, if the wallet has under
wallet_min_closure_btc
remaining, it transfers that BTC to a randomly
selected wallet and closes.
Each redemption request is identified by a concatenation of the wallet’s pubkey hash and redeemer’s output hash (redeemer’s BTC address). Such an identifier allows retrieving pending redemption requests in a gas-efficient way based on information provided in the redemption proof. A consequence of this approach is that the redeemer can not use the same redemption BTC address when there is already one redemption request pending from the given wallet. User experience can be improved by the dApp by selecting the previous-oldest wallet without a pending redemption request to the given BTC address in case a new redemption is requested before the pending one gets cleared out.
At a minimum, pending redemption request needs to capture the following information:
-
Expected range of value for redemption UTXO: needed to validate the amount released by the system when processing the redemption proof. Fees are governable and the expected amount redeemed needs to be captured at the moment of the redemption request.
-
Ethereum address of the redeemer: in case anything goes wrong with the redemption, this address will be used to return the unprocessed balance.
-
UNIX timestamp at which the redemption was requested: needed to validate redemption timeout.
There is a governable redemption request timeout value,
redemption_request_timeout
. If redemption proof was not submitted and
timeout is exceeded, the wallet is slashed and balances are returned back to the
redeemer, no matter if Bitcoin was released or not. The system is as
decentralized as the least decentralized element of it. Redeemers need to have
certain guarantees provided by the smart contract and no external body should
judge if the Bitcoin was spent or not. It is the wallet’s responsibility to
process redemption requests in time.
Just like in the case of sweeps, off-chain clients should wait for enough redemption requests from the given wallet to accumulate and process them in batches in order to minimize gas expenditure of redemption proofs. At the same time, the redeemer does not care about the underlying mechanism and they want to have their particular redemption request processed in time. Given that it is the individual redemption request that is timing out and not the entire batch of redemption requests, the wallet takes a certain risk on itself by waiting and not processing individual redemption requests immediately. At the same time, in case of a high number of redemption requests, the wallet is incentivized to process them in batches to minimize the wait time for Bitcoin and Ethereum chain confirmations between redemptions. To minimize the risk for the wallet and to do not disincentivize it for waiting for enough redemption requests to accumulate, the redemption timeout should be long enough, for example, 72 hours and ideally even longer in the early days of the system.
In the case of a timed-out redemption request the wallet is ordered to move the BTC to another random wallet. The wallet that timed out on processing redemption request can not be requested for another redemption. Operators are slashed for a timeout but they continue to earn rewards.
Based on the information provided in the submitted redemption proof, pending redemption requests possibly satisfied by the proof should be retrieved and each of them should be validated separately.
If a pending redemption request exists for every UTXO of the redemption transaction but the main wallet UTXO, and BTC value of each UTXO is within the expected range, redemption proof is accepted and redeemed Bitcoin balances in the Bridge are cleared out. It is important to note that timeout is not validated in this function. That is, if the timeout was not reported, the wallet can process the entire redemption proof successfully even though the timeout for the oldest redemption request already passed.
If a pending redemption request for the given UTXO does not exist, and that redemption request was not earlier reported as timed-out or fraudulent because of the UTXO value not being within the expected range, the entire redemption proof transaction should revert.
If the value of the given UTXO matching pending redemption request is not in the expected range, this is a fraud, and the entire redemption proof transactions should revert.
If a pending redemption request for the given UTXO does not exist but that redemption request was earlier reported as timed-out, that UTXO is skipped, and the redemption proof transaction gets accepted, assuming there are more UTXOs in the redemption transaction.
Submit redemption proof function updates the main wallet UTXO. Bitcoin balances in the bridge are cleared out for successfully processed redemption requests.
Redemption proof for the given timed-out redemption request is accepted and processed unless that redemption request was reported as timed out. Anyone can report redemption timeout but there is no reward or gas cost reimbursement for doing it so only the redeemer is incentivized to report the timeout.
When redemption request timeout is reported, the redeemer receives their balance
back, no matter if the underlying Bitcoin was redeemed or not. Each wallet
member is slashed for redemption_request_timeout_slashing_amount
and redeemer
receives a percentage of a misbehavior notifier reward, as specified in
redemption_request_timeout_redeemer_bonus
. Redemption request is removed from
the list of pending redemption requests and it is marked as timed-out. This way,
when redemption proof gets submitted, the timed-out redemption request will be
skipped. This mechanism allows proving whatever can still be proved and
unblocking the wallet by updating its main UTXO.
The transaction must revert if the given redemption request was already reported as timed out.
The wallet is ordered to move the BTC to another random wallet and is no longer accepting redemption requests.
Governable Parameters:
-
fraud_slashing_amount
: The amount of stake slashed from each member of a wallet for a fraud. -
fraud_notifier_reward_multiplier
: The percentage of the notifier reward from the staking contract the notifier of a fraud receives. -
fraud_challenge_defend_timeout
: The amount of time the wallet has to defend against a fraud challenge. -
fraud_challenge_deposit_amount
: The amount of ETH the party challenging the wallet for fraud needs to deposit.
For every UTXO spent by the wallet in an incorrect way, anyone should be able to provide a fraud proof. Given that a Bitcoin transaction could be so large that proving it on Ethereum would be impossible, fraud proofs need to be processed with a challenge-response approach.
When a wallet unlocks a UTXO it needs to calculate a sighash and provide a signature over that sighash, one for each unlocked UTXO. Fraud is reported for a UTXO by providing a sighash along with the wallet’s signature over that sighash. From that moment, the wallet has a certain time to defend itself against the challenge and prove that the UTXO was spent in an honest way.
UTXO unlocked by the wallet is spent in a fraudulent way if:
-
that unlocked UTXO is a revealed deposit that was not proved as swept and can not be proved as swept, or
-
that unlocked UTXO was not and is not the main wallet’s UTXO.
The wallet is allowed to execute only transactions that are accepted by the
submit sweep proof, submit redemption proof, or submit funds migrated functions.
All other transactions are considered fraud. In other words, if the wallet signed
some UTXO, it needs to use that UTXO, and prove to the Bridge this is a valid
usage. If none of it happens within the fraud_challenge_defeat_timeout
the
wallet has to defend against the challenge, this is considered a fraud.
The consequence of this approach is that we need to track all UTXOs spent by the wallet next to the main wallet’s UTXO:
// wallet pubkey hash to the current main's UTXO hash computed
// as keccak256(txHash | txOutputIndex | txOutputValue)
mapping(bytes20 => bytes32) public mainUtxos;
// spent main UTXO hash computed as keccak256(txHash | txOutputIndex)
mapping(bytes32 => bool) spentMainUTXOs
Note that this approach is stricter than just validating the public key in the UTXO. For example, if we were considering that a collection of deposit UTXO’s plus the main UTXO for the wallet unlocked using the wallet public key and locked under a single UTXO using the same wallet public key is not a fraud but a normal sweep, the Bridge would be susceptible to attacks when the malicious wallet steals revealed deposits by sweeping them to another UTXO but not the main UTXO known by the Bridge.
To protect against chain reorgs capable of causing good-faith transactions to be indefensible against fraud proofs, the wallet needs to wait for enough block confirmations before undertaking any action. There has to be a reasonable compromise between finality and responsivity and suggested values are 6 confirmations on Bitcoin and 40 confirmations on Ethereum.
Anyone should be able to submit a fraud challenge. At a minimum, the function should accept the wallet’s signature and the sighash uniquely identifying UTXO unlocked by the wallet. The function should validate the wallet state to make sure it’s neither closed nor terminated. It should also validate if the signature over the sighash is valid and if it belongs to the wallet.
The function should require the challenger to provide a deposit in ETH equal to
fraud_challenge_deposit_amount
that is returned to the challenger once the fraud
is confirmed. If the wallet defends against the challenge, the deposit is sent
to the treasury. If the wallet does not defend against the challenge, the
challenger receives misbehavior notifier reward based on
fraud_notifier_reward_multiplier
and the wallet gets slashed based on
fraud_slashing_amount
.
Anyone can defend the wallet against the fraud challenge by submitting a transaction preimage matching the challenged sighash and proving that transaction was valid to the protocol.
The function allowing to defend against the challenge should first validate the submitted preimage against challenged sighash and try to locate the revealed deposit for the input corresponding to the sighash.
If there is a revealed deposit matching the sighash and that deposit was swept by the Bridge, everything is fine and the wallet defended itself against the challenge.
If there is a revealed deposit matching the sighash and it was not yet swept by the Bridge, the defend function should revert. The wallet should first submit the sweep proof for that deposit and then call the defend function again.
If the revealed deposit does not exist, the defend function should check if the sighash
belongs to one of the wallet’s spent UTXOs from spentMainUTXOs
mapping. If it does,
everything is fine and the wallet defended itself against the challenge. If it
does not, the defend function reverts. The wallet should submit the redemption
proof and then, call the defend function again.
Governable Parameters:
-
wallet_creation_period
: How frequently we attempt to create new wallets. -
wallet_min_creation_btc
: The minimum amount of BTC an active wallet needs to have before we allow for the creation of a new active wallet. -
wallet_max_age
: The oldest we allow a wallet to become before we transfer the funds to a randomly selected wallet.
A new wallet is created when enough time has passed as defined in
wallet_creation_period
AND the wallet contains at least
wallet_min_creation_btc
btc. To create a new wallet, a group of 100 operators
is selected from the pool of available operators using a process called
sortition. The probability that a particular operator is chosen is based on
their stake weight, which in turn is based on the number of T
tokens they
have invested in the staking contract.
Once the operators have been selected from the sortition pool, they generate a 51-of-100 ecdsa signing group to handle the bitcoin key material per the process described in RFC 2: tBTCv2 Group Selection and Key Generation. The group size may end up being smaller depending on retries.
As time passes and operators drop out of the system, a wallet becomes at risk
of being able to meet the 51-of-100 threshold to produce signatures.
Additionally, we want to avoid situations where operators are the custodians of
a wallet for extended periods. To avoid these issues, once a wallet is older
than the wallet_max_age
, or if it drops below the liveness threshold (say,
below 70 on a heartbeat), we motion to
transfer the funds to another randomly selected wallet.
Once a wallet no longer has funds and is not the primary wallet for new deposits, it can be closed and operators are no longer required to maintain it.
Governable Parameters:
-
heartbeat
: The number of group members required for a heartbeat to be successful. -
wallet_closure_timeout
: The amount of time that a wallet has to successfully move its funds to another wallet and inform the ethereum chain before it is at risk of punishment. -
wallet_dust_leftover
: The smallest amount of btc that we will transfer to another wallet when a wallet closes. Any amount under this is abandoned. -
wallet_max_age
: The oldest we allow a wallet to become before we transfer the funds to a randomly selected wallet. -
wallet_min_closure_btc
: The smallest amount of btc a wallet can hold before we attempt to close the wallet and transfer the funds to a randomly selected wallet.
When a wallet fails a heartbeat consecutive_failed_heartbeats
times, ends a redemption with less than wallet_min_closure_btc
remaining, or exceeds the wallet_max_age
, then a few different parties can
begin the process of closing it.
If the wallet failed a heartbeat, then the first operator
posts a reimbursable transaction to the ethereum chain
declaring an intention to close the wallet. If the wallet exceeds
wallet_max_age
or the funds fall under wallet_min_closure_btc
, then
the public can post that transaction. If the wallet failed
a redemption, then the public (probably the redeemer) can post a
punishment transaction.
If the balance is less than wallet_dust_leftover
, we simply close the wallet
and abandon the funds, signaling this with a public
transaction, and can maintain the peg via a donation.
Otherwise, we attempt to move the funds to other active wallet(s).
The first operator posts a signed, reimbursable
intention-to-move
transaction to the ethereum chain declaring which wallets
the operators intend to move the funds to. We trust the wallet to do this
correctly, because if it was malicious, it has little reason to not just steal
the funds. Then, that operator proposes a BTC fee (as in sweeping)
and the operators transfer the funds evenly between the wallets proposed in the
intention-to-move
transaction. They construct a P2PKH transaction moving the
wallet’s main UTXO to each of those wallets. For more details on exactly how a
wallet is chosen and how the funds are split up see
the dedicated section.
After this transaction is complete, the public is able to
submit a SPV proof to let ethereum know that the funds were transferred
nonfraudulently (they match the intention-to-move
wallets, the fee isn’t too
high, and the funds are split evenly). Later, a donation can be
made by governance to handle any difference in outstanding v2 token supply and
locked BTC due to mining transfer fees.
Notes: Transferring the BTC to any address other than the P2PKH of one of the
other wallet addresses is fraud, and is punishable. Moving funds
without sending a follow-up proof is punishable. Failure to
close a wallet that failed a heartbeat or fell below
wallet_min_closure_btc
, or exceeded wallet_max_age
is
punishable. Failure to complete the process before
wallet_closure_timeout
has elapsed is punishable.
Any unswept funds can be returned via the 30-day return script, though since we’re redeeming from the oldest wallet and only sweeping/depositing to the newest wallets this hopefully won’t come up often 🤞.
Governable Parameters:
-
wallet_transfer_max
: The most amount of BTC a wallet can transfer to a single other wallet during a wallet closure.
When a wallet closes, if there is any amount of bitcoin
remaining, it needs to be transferred to another live wallet. Our input to the
transaction is the closing wallet’s main UTXO, and we create an equal sized
output for the N
valid wallets, where N = min(valid_wallets.size,
ceil(funds_to_transfer / wallet_transfer_max))
.
The destination wallets are chosen for their public key hash’s modulus distance to the closing wallet’s public key hash (both represented as a number). Ties are broken by the destination with the greater public key hash, and if we run out of viable wallets, it is okay to reuse them (though we prefer not to).
For all the below examples, say that the public key hashes can range from 0 to 99 (mod 100). Wallet#43 Means that the PKH of the Wallet is 43. Real public key hashes and their max mod values will be much higher in practice!
Exceeding Transfer Max Example:
-
Wallet#90 with balance
250 BTC
needs to close. -
wallet_transfer_max = 100 BTC
-
Live wallet options are Wallet#30, Wallet#80, Wallet#25, Wallet#15
-
Calculate distances:
-
Wallet#30 = min(90 - 30, 130 - 90) = min(60, 40) = 40
-
Wallet#80 = min(90 - 80, 180 - 90) = min(10, 90) = 10
-
Wallet#25 = min(90 - 25, 125 - 90) = min(65, 35) = 35
-
Wallet#15 = min(90 - 15, 115 - 90) = min(75, 25) = 25
N = min([Wallet#30, Wallet#80, Wallet#25, Wallet#15].size, ceil(250 / 100)) = min(4, ceil(2.5)) = min(4, 3) = 3
Split the 250 BTC 3 ways. Deposit 250/3 = 83 1/3 BTC into Wallet#80, Wallet#15, and Wallet#25.
Under Transfer Max Example:
-
Wallet#52 with balance
50 BTC
needs to close. -
wallet_transfer_max = 100 BTC
-
Live wallet options are Wallet#13, Wallet#90, Wallet#59, Wallet#42
-
Calculate distances:
-
Wallet#13 = min(52 - 13, 113 - 52) = min(39, 61) = 39
-
Wallet#90 = min(90 - 52, 152 - 90) = min(38, 62) = 38
-
Wallet#59 = min(59 - 52, 152 - 59) = min(7, 93) = 7
-
Wallet#42 = min(52 - 42, 142 - 52) = min(10, 90) = 10
N = min([Wallet#13, Wallet#90, Wallet#59, Wallet#42].size, ceil(50/100)) = min(4, ceil(0.5) = min(4,1) = 1)
Split the 50 BTC 1 way. Deposit 50/1 = 50 BTC into Wallet#59.
Tiebreak Example:
-
Wallet#52 with a balance
13 BTC
needs to close. -
wallet_transfer_max = 100 BTC
-
Live wallet options are Wallet#13, Wallet#60, Wallet#44, Wallet#75
-
Calculate distances:
-
Wallet#13 = min(52 - 13, 113 - 52) = min(39, 61) = 39
-
Wallet#60 = min(60 - 52, 152 - 60) = min(8, 92) = 8
-
Wallet#44 = min(52 - 44, 144 - 52) = min(8, 92) = 8
-
Wallet#75 = min(75 - 52, 152 - 75) = min(23, 77) = 23
N = min([Wallet#13, Wallet#60, Wallet#44, Wallet#75].size, ceil(13/100)) = min(4, ceil(0.13) = min(4, 1) = 1)
Split the 13 BTC 1 way. Since Wallet#60 is tied with Wallet#44 for the closest, we favor Wallet#60
because 60>44. Deposit 13/1 = 13 BTC into Wallet#60.
Wallet Reuse Example:
-
Wallet#35 with a balance
413 BTC
needs to close. -
wallet_transfer_max = 100 BTC
-
Live wallet options are Wallet#20, Wallet#30
-
Calculate distances:
-
Wallet#20 = min(35 - 20, 120 - 35) = min(15, 85) = 15
-
Wallet#30 = min(35 - 30, 130 - 35) = min(5, 95) = 5
N = min([Wallet#20, Wallet#30].size, ceil(413/100)) = min(2, ceil(4.13)) = min(2, 5) = 2
Split the 413 BTC 2 ways. Deposit 413/2 = 206.5 BTC into Wallet#30 and Wallet#20.
Governable Parameters:
-
consecutive_failed_heartbeats
: The number of times a heartbeat can fail in a row that triggers a move to close the wallet. -
failed_heartbeat_reward_removal_period
: The amount of time an operator is removed from reward eligibility after failing a heartbeat. -
heartbeat
: The number of group members required for a heartbeat to successful. -
heartbeat_block_length
: The number of ethereum blocks until the next heartbeat. If set to 40, then the signers sign every 40th block.
To make sure that older wallets are still accessible for redemption, we need to
perform heartbeats. The signing group signs a block when block count mod
heartbeat_block_length
= 0 and then does not publish the result. If there
are ever less than heartbeat
operators that participate in the heartbeat, the
active operators record the inactive operators. If this happens a number of
times >= consecutive_failed_heartbeats
, the operators take a union the
recorded inactive operators, and an operator from among the active operators
posts a transaction to disable those inactive operators from receiving rewards
for failed_heartbeat_reward_removal_period
amount of time. The active
operators move the remaining BTC to another random wallet and
close this wallet.
For the purposes of heartbeats, an operator that is currently unstaking (they started their two-week undelegation period) does not count as a live heartbeat, but also does not count as an inactive operator. Thus, operators who are unstaking might cause a wallet to fail a heartbeat, but can still be around to help move the funds to another wallet, and would not have their rewards turned off.
To represent this, clients should monitor the ethereum chain for unstaking events to keep track of which operators are active/unstaking. Unstaking operators should continue to send heartbeats. Operators ignore heartbeats from unstaking operators for the purposes of determining wallet liveness. Operators don’t ignore heartbeats from unstaking operators for determining reward eligibility.
Moving the funds costs a bitcoin mining fee, as well as a transaction on the ethereum side to prove this happened. To maintain the peg, we need to reduce the equivalent amount of fee from the treasury’s account balance (set aside for such fees). This will need to be proven and updated ethereum-side, and properly incentivized.
Example:: Say that heartbeat = 70, consecutive_failed_heartbeats = 1
.
Operator-1 through Operator-72 are all active while Operator-73 through
Operator-100 are inactive. Currently, there are 72 active operators, so the
heartbeat check is passing. Then, Operator-25 through Operator-30 decide to
unstake. Since unstaking operators do not count as a live heartbeat, there
would only be 67 heartbeats, and the wallet would begin to transfer its funds.
An operator from Operator-1 through Operator-72 (including 25-30) chooses to publish that Operator-73 through Operator-100 should have their rewards disabled (after the active operators sign this). See transaction incentives for more on how to encourage this transaction.
If a wallet committed fraud, there has to be a function allowing to "donate" BTC to the Bridge without increasing anyone’s balances. This function needs to validate SPV proof of the donate transaction and update the main wallet UTXO. This function should also be used to donate the Bridge to compensate for Bitcoin fees burned on moving funds between wallets that failed a heartbeat.
The BTC to donate needs to come from the coverage pool funds. The DAO - or some delegate of the DAO - should be able to claim the coverage from the pool and manually liquidate tokens to acquire BTC and donate it to the Bridge.
Governable Parameters:
-
max_gas_refund_price
: The highest amount of gwei that the gas refund contract will pay out per gas for a refund transaction.
Transaction incentives are more deeply explored in RFC 6: Transaction Incentives. Summarized:
There are three different types of transactions: Operator-Only, Public-Knowledge, and Punishment.
Operator-Only transactions are where only the operators have access to the information required to assemble the transaction with the right input parameters.
In order to avoid all operators racing to submit the transaction at the same time, we have an off-chain informal agreement to submit based on the operator’s position in the group (can use the hash of the group’s pubkey).
If the designated operator does not submit their transaction before a timeout expires, the duty moves to the next operator and the group can sign a transaction to mark that operator as inactive. Since there is no slashing reward, and since this transaction can only be submitted by an operator, this transaction is also Operator-Only.
In order to compensate the operator for posting the transaction, the gas spent
will be reimbursed by a DAO-funded eth pool in the same transaction, limited by
max_gas_refund_price
.
Public-Knowledge transactions are where anyone has access to the information required to assemble the transaction.
In order to prevent wasting gas on racing to submit, we can either use an off-chain informal agreement for the operators like in Operator-Only transactions, or we can delegate the transactions to a network like Gelato. We can’t cost-effectively stop members of the public from trying to race to submit.
To compensate these transactions, whoever posts them will have the gas spent
reimbursed by a DAO-funded eth pool in the same transaction, limited by
max_gas_refund_price
.
Punishment transactions are where anyone has access to the information required to assemble the transaction (like Public-Knowledge) and the transaction leads to the potential for punishment (reward ineligibility or slashing).
In these transactions, maintaining system health is more important than optimizing gas via preventing racing, so we offer up bounties in the form of potentially slashed tokens to whichever submitter submits first. We do not compensate gas.
Governable Parameters:
-
btc_fee_broadcast_timeout
: The amount of time an operator has to provide a suggested BTC fee before the other operators give up and try the next operator.
Any time a bitcoin transaction needs to be posted and then mined on the bitcoin blockchain, the miners need to be paid a fee for their work. This fee fluctuates with market demand and is decently volatile.
The operators need to all agree on a fee before they can construct the sweeping
transaction, but communicating that fee is tricky. When the sortition pool
selects the operators, it selects them in an ordered list. We can leverage this
on-chain order to be the fee-proposal-order. Say that the sortition pool chose
Operators: [#71, #109, #34…, #2]. When it comes time to sweep deposits,
Operator#71 would be expected to query
https://blockstream.info/api/fee-estimates for the 3-block fee and broadcast
this fee to the rest of the operators. If the operators don’t receive the fee
from operator #71 before btc_fee_broadcast_timeout
, has elapsed, the duty
moves to #109, and then #34, and so on.
Note: Each operator may only propose one BTC fee per sweep (that the other operators will listen to) or we enable them to spam the communications with fee proposals until they find one that passes validation.
The operators then validate the fee they received against
https://blockstream.info/api/fee-estimates to make sure that nothing fishy is
going on - that the fee isn’t too high or too low (TBD what that means). If it
is, they can wait for btc_fee_broadcast_timeout
to elapse and for a new fee
to be proposed.
Note: We purposefully leave out on-chain incentives/punishments here. The attack vector is small and the overhead is high. We might need to revisit this in the future, but if someone maintained a client fork in order to take advantage of manipulating btc fee consensus I would be very surprised.
Alphabetized list of Governable Parameters with additional notes.
-
base_btc_fee_max
: The highest amount of BTC that operators can initially propose as a fee for miners of Bitcoin transaction. -
btc_fee_broadcast_timeout
: The amount of time an operator has to provide a suggested BTC fee before the other operators give up and try the next operator. -
btc_fee_max
: The highest amount of BTC that operators can eventually propose as a fee for miners of Bitcoin transaction. -
consecutive_failed_heartbeats
: The number of times a heartbeat can fail in a row that triggers a move to close the wallet. -
dust_threshold
: The minimum bitcoin deposit amount for the transaction to be considered for a sweep. -
failed_heartbeat_reward_removal_period
: The amount of time an operator is removed from reward eligibility after failing a heartbeat. -
fraud_challenge_defend_timeout
: The amount of time the wallet has to defend against a fraud challenge. -
fraud_challenge_deposit_amount
: The amount of ETH the party challenging the wallet for fraud needs to deposit. -
fraud_notifier_reward_multiplier
: The percentage of the notifier reward from the staking contract the notifier of a fraud receives. -
fraud_slashing_amount
: The amount of stake slashed from each member of a wallet for a fraud. -
heartbeat_block_length
: The number of ethereum blocks until the next heartbeat. If set to 40, then the signers sign every 40th block. -
heartbeat
: The number of group members required for a heartbeat to be successful. -
max_gas_refund_price
: The highest amount of gwei that the gas refund contract will pay out per gas for a refund transaction. -
redemption_request_timeout_redeemer_bonus_multiplier
: The percentage of the notifier reward from the staking contract the redeemer receives in case of a redemption timeout. -
redemption_request_timeout_slashing_amount
: The amount of stake slashed from each member of a wallet for a timed-out redemption request. -
redemption_request_timeout
: The amount of time the wallet has to provide redemption proof. -
redemption_treasury_fee
: The percentage of redeemed amount put aside as a treasury fee. -
skip_sweep_timeout
: The amount of time the depositor has to reimburse the operator for the gas of the sweep and collect their account balance. -
sweep_max_deposits
: The number of non-dust unswept revealed bitcoin deposits that will trigger an early sweep on a wallet. -
sweep_period
: The amount of time we wait between scheduled sweeps on a wallet. -
sweeping_fee_bump_period
: The amount of time we wait to see if a sweeping transaction is mined before increasing the fee. -
sweeping_fee_max_multiplier
: The highest we will try to increment the fee multiplier to before giving up and picking a new base fee and different deposits to sweep. -
sweeping_fee_multiplier_increment
: The amount we add to the sweeping fee multiplier each time a sweeping transaction is not mined within thesweeping_fee_bump_period
. For example, if this param is set to 0.2 and we are currently at 1.6x, then the next time we would try 1.8x. -
sweeping_refund_safety_time
: The amount of time prior to when a UTXO becomes eligible for a refund where we will not include it in a sweeping transaction. -
wallet_closure_timeout
: The amount of time that a wallet has to successfully move its funds to another wallet and inform the ethereum chain before it is at risk of punishment. -
wallet_creation_period
: How frequently we attempt to create new wallets. -
wallet_dust_leftover
: The smallest amount of btc that we will transfer to another wallet when a wallet closes. Any amount under this is abandoned. -
wallet_max_age
: The oldest we allow a wallet to become before we transfer the funds to a randomly selected wallet. -
wallet_min_closure_btc
: The smallest amount of BTC a wallet can hold before we attempt to close the wallet and transfer the funds to a randomly selected wallet. -
wallet_min_creation_btc
: The minimum amount of BTC an active wallet needs to have before we allow for the creation of a new active wallet. -
wallet_transfer_max
: The most amount of BTC a wallet can transfer to a single other wallet during a wallet closure.