-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
func-test: add bridge withdraw reassignment
- Loading branch information
Showing
2 changed files
with
310 additions
and
441 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,282 +1,73 @@ | ||
import time | ||
|
||
import flexitest | ||
from bitcoinlib.services.bitcoind import BitcoindClient | ||
from strata_utils import ( | ||
deposit_request_transaction, | ||
extract_p2tr_pubkey, | ||
get_balance, | ||
) | ||
from web3 import Web3 | ||
from web3.middleware import SignAndSendRawMiddlewareBuilder | ||
|
||
import testenv | ||
from constants import ( | ||
DEFAULT_ROLLUP_PARAMS, | ||
PRECOMPILE_BRIDGEOUT_ADDRESS, | ||
SATS_TO_WEI, | ||
UNSPENDABLE_ADDRESS, | ||
from constants import UNSPENDABLE_ADDRESS | ||
from fn_bridge_withdraw_happy import ( | ||
DEPOSIT_AMOUNT, | ||
check_initial_eth_balance, | ||
deposit_twice, | ||
do_withdrawal, | ||
setup_services, | ||
) | ||
from utils import get_bridge_pubkey, wait_until | ||
|
||
# Local constants | ||
# D BTC | ||
DEPOSIT_AMOUNT = DEFAULT_ROLLUP_PARAMS["deposit_amount"] | ||
# Gas for the withdrawal transaction | ||
WITHDRAWAL_GAS_FEE = 22_000 # technically is 21_000 | ||
# Ethereum Private Key | ||
# NOTE: don't use this private key in production | ||
ETH_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001" | ||
# BTC Operator's fee for withdrawal | ||
OPERATOR_FEE = DEFAULT_ROLLUP_PARAMS["operator_fee"] | ||
# BTC extra fee for withdrawal | ||
WITHDRAWAL_EXTRA_FEE = DEFAULT_ROLLUP_PARAMS["withdraw_extra_fee"] | ||
|
||
|
||
@flexitest.register | ||
class BridgeWithdrawHappyTest(testenv.StrataTester): | ||
class BridgeWithdrawReassignmentTest(testenv.StrataTester): | ||
""" | ||
Makes two DRT deposits to the same EL address, then makes a withdrawal to a change address. | ||
Checks if the balance of the EL address is expected | ||
and if the BTC balance of the change address is expected. | ||
Makes two DRT deposits, then triggers the withdrawal. | ||
The bridge client associated with assigned operator id is stopped. | ||
After the dispatch assignment duration is over, | ||
Check if new operator is being assigned or not | ||
""" | ||
|
||
def __init__(self, ctx: flexitest.InitContext): | ||
# Example: we spin up 5 operators | ||
ctx.set_env(testenv.BasicEnvConfig(101, n_operators=5)) | ||
|
||
def main(self, ctx: flexitest.RunContext): | ||
address = ctx.env.gen_ext_btc_address() | ||
withdraw_address = ctx.env.gen_ext_btc_address() | ||
self.debug(f"Address: {address}") | ||
self.debug(f"Change Address: {withdraw_address}") | ||
self.debug(f"Gas: {WITHDRAWAL_GAS_FEE}") | ||
|
||
btc = ctx.get_service("bitcoin") | ||
seq = ctx.get_service("sequencer") | ||
reth = ctx.get_service("reth") | ||
# we need both bridge to know assignment path | ||
bridge_clients = [ctx.get_service(f"bridge.{num}") for num in range(5)] | ||
|
||
seqrpc = seq.create_rpc() | ||
btcrpc: BitcoindClient = btc.create_rpc() | ||
rethrpc = reth.create_rpc() | ||
bridge_rpcs = [client.create_rpc() for client in bridge_clients] | ||
|
||
seq_addr = seq.get_prop("address") | ||
self.debug(f"Sequencer Address: {seq_addr}") | ||
|
||
btc_url = btcrpc.base_url | ||
btc_user = btc.props["rpc_user"] | ||
btc_password = btc.props["rpc_password"] | ||
|
||
self.debug(f"BTC URL: {btc_url}") | ||
self.debug(f"BTC user: {btc_user}") | ||
self.debug(f"BTC password: {btc_password}") | ||
|
||
# Get the original balance of the withdraw address | ||
original_balance = get_balance(withdraw_address, btc_url, btc_user, btc_password) | ||
self.debug(f"BTC balance before withdraw: {original_balance}") | ||
|
||
web3: Web3 = reth.create_web3() | ||
# Create an Ethereum account from the private key | ||
eth_account = web3.eth.account.from_key(ETH_PRIVATE_KEY) | ||
el_address = eth_account.address | ||
self.debug(f"EL address: {el_address}") | ||
btc, seq, reth, seqrpc, btcrpc, rethrpc, web3, el_address = setup_services(ctx, self.debug) | ||
|
||
# Add the Ethereum account as auto-signer | ||
# Transactions from `el_address` will then be signed, under the hood, in the middleware | ||
web3.middleware_onion.inject(SignAndSendRawMiddlewareBuilder.build(eth_account), layer=0) | ||
# Check initial balance is 0 | ||
check_initial_eth_balance(rethrpc, el_address, self.debug) | ||
|
||
# Get the balance of the EL address before the deposits | ||
balance = int(rethrpc.eth_getBalance(el_address), 16) | ||
self.debug(f"Strata Balance before deposits: {balance}") | ||
assert balance == 0, "Strata balance is not expected" | ||
# Perform two deposits | ||
deposit_twice(ctx, web3, seqrpc, rethrpc, el_address, self.debug) | ||
|
||
# Gas price | ||
gas_price = web3.to_wei(1, "gwei") | ||
self.debug(f"Gas price: {gas_price}") | ||
|
||
# Get operators pubkey and musig2 aggregates it | ||
bridge_pk = get_bridge_pubkey(seqrpc) | ||
self.debug(f"Bridge pubkey: {bridge_pk}") | ||
|
||
# Deposit to the EL address | ||
# NOTE: we need 2 deposits to make sure we have funds for gas | ||
self.make_drt(ctx, el_address, bridge_pk) | ||
self.make_drt(ctx, el_address, bridge_pk) | ||
balance_after_deposits = int(rethrpc.eth_getBalance(el_address), 16) | ||
self.debug(f"Strata Balance after deposits: {balance_after_deposits}") | ||
wait_until( | ||
lambda: int(rethrpc.eth_getBalance(el_address), 16) == 2 * DEPOSIT_AMOUNT * SATS_TO_WEI | ||
) | ||
|
||
# Get the balance of the EL address after the deposits | ||
balance = int(rethrpc.eth_getBalance(el_address), 16) | ||
self.debug(f"Strata Balance after deposits: {balance}") | ||
assert balance == 2 * DEPOSIT_AMOUNT * SATS_TO_WEI, "Strata balance is not expected" | ||
|
||
# Send funds to the bridge precompile address for a withdrawal | ||
change_address_pk = extract_p2tr_pubkey(withdraw_address) | ||
self.debug(f"Change Address PK: {change_address_pk}") | ||
estimated_withdraw_gas = self.estimate_withdraw_gas( | ||
ctx, web3, el_address, change_address_pk | ||
# Perform a withdrawal | ||
l2_tx_hash, tx_receipt, total_gas_used = do_withdrawal( | ||
web3, rethrpc, el_address, withdraw_address, self.debug, DEPOSIT_AMOUNT | ||
) | ||
self.debug(f"Estimated withdraw gas: {estimated_withdraw_gas}") | ||
l2_tx_hash = self.make_withdraw( | ||
ctx, web3, el_address, change_address_pk, estimated_withdraw_gas | ||
).hex() | ||
self.debug(f"Sent withdrawal transaction with hash: {l2_tx_hash}") | ||
|
||
# Wait for the withdrawal to be processed | ||
wait_until(lambda: web3.eth.get_transaction_receipt(l2_tx_hash)) | ||
tx_receipt = web3.eth.get_transaction_receipt(l2_tx_hash) | ||
self.debug(f"Transaction receipt: {tx_receipt}") | ||
total_gas_used = tx_receipt["gasUsed"] * tx_receipt["effectiveGasPrice"] | ||
self.debug(f"Total gas used: {total_gas_used}") | ||
|
||
# Check assigned operator | ||
duties = seqrpc.strata_getBridgeDuties(0, 0)["duties"] | ||
print(duties) | ||
withdraw_duty = [duty for duty in duties if duty["type"] == "FulfillWithdrawal"] | ||
withdraw_txid = "" | ||
assigned_operator = bridge_clients[0] | ||
for duty in withdraw_duty: | ||
withdraw_txid = duty["payload"]["deposit_outpoint"].split(":")[0] | ||
dutyStatus = bridge_rpcs[0].stratabridge_getDutyStatus(withdraw_txid) | ||
print("duty status is") | ||
print(dutyStatus) | ||
dutyStatus = bridge_rpcs[1].stratabridge_getDutyStatus(withdraw_txid) | ||
print("duty status is") | ||
print(dutyStatus) | ||
assigned_operator = bridge_clients[duty["payload"]["assigned_operator_idx"]] | ||
|
||
# Make sure that the balance is expected | ||
balance_post_withdraw = int(rethrpc.eth_getBalance(el_address), 16) | ||
self.debug(f"Strata Balance after withdrawal: {balance_post_withdraw}") | ||
difference = DEPOSIT_AMOUNT * SATS_TO_WEI - total_gas_used | ||
self.debug(f"Strata Balance difference: {difference}") | ||
assert difference == balance_post_withdraw, "balance difference is not expected" | ||
withdraw_duty = [d for d in duties if d["type"] == "FulfillWithdrawal"][0] | ||
assigned_op_idx = withdraw_duty["payload"]["assigned_operator_idx"] | ||
assigned_operator = ctx.get_service(f"bridge.{assigned_op_idx}") | ||
self.debug(f"Assigned operator index: {assigned_op_idx}") | ||
|
||
print("assigned operator is stopped") | ||
print(assigned_operator) | ||
# Stop assigned operator | ||
self.debug("Stopping assigned operator ...") | ||
assigned_operator.stop() | ||
|
||
btcrpc.proxy.generatetoaddress(150, UNSPENDABLE_ADDRESS) | ||
time.sleep(5) | ||
duties = seqrpc.strata_getBridgeDuties(0, 0)["duties"] | ||
print(duties) | ||
|
||
# Wait for the withdraw address to have a positive balance | ||
self.mine_blocks_until_maturity( | ||
btcrpc, withdraw_address, btc_url, btc_user, btc_password, original_balance | ||
) | ||
|
||
# Make sure that the balance in the BTC wallet is D BTC - operator's fees | ||
btc_balance = get_balance(withdraw_address, btc_url, btc_user, btc_password) | ||
self.debug(f"BTC balance: {btc_balance}") | ||
difference = DEPOSIT_AMOUNT - OPERATOR_FEE - WITHDRAWAL_EXTRA_FEE | ||
self.debug(f"BTC expected balance: {original_balance + difference}") | ||
assert btc_balance == original_balance + difference, "BTC balance is not expected" | ||
|
||
return True | ||
|
||
def make_drt(self, ctx: flexitest.RunContext, el_address, musig_bridge_pk): | ||
""" | ||
Deposit Request Transaction | ||
""" | ||
# Get relevant data | ||
btc = ctx.get_service("bitcoin") | ||
seq = ctx.get_service("sequencer") | ||
btcrpc: BitcoindClient = btc.create_rpc() | ||
btc_url = btcrpc.base_url | ||
btc_user = btc.props["rpc_user"] | ||
btc_password = btc.props["rpc_password"] | ||
seq_addr = seq.get_prop("address") | ||
|
||
# Create the deposit request transaction | ||
tx = bytes( | ||
deposit_request_transaction( | ||
el_address, musig_bridge_pk, btc_url, btc_user, btc_password | ||
) | ||
).hex() | ||
self.debug(f"Deposit request tx: {tx}") | ||
|
||
# Send the transaction to the Bitcoin network | ||
txid = btcrpc.proxy.sendrawtransaction(tx) | ||
self.debug(f"sent deposit request with txid = {txid} for address {el_address}") | ||
time.sleep(1) | ||
|
||
# time to mature DRT | ||
btcrpc.proxy.generatetoaddress(6, seq_addr) | ||
# Let enough blocks pass so the assignment times out | ||
# (64 is the dispatch_assignment_duration) | ||
btcrpc.proxy.generatetoaddress(64, UNSPENDABLE_ADDRESS) | ||
time.sleep(3) | ||
|
||
# time to mature DT | ||
btcrpc.proxy.generatetoaddress(6, seq_addr) | ||
time.sleep(3) | ||
|
||
def make_withdraw( | ||
self, | ||
ctx: flexitest.RunContext, | ||
web3: Web3, | ||
el_address, | ||
change_address_pk, | ||
gas=WITHDRAWAL_GAS_FEE, | ||
): | ||
""" | ||
Withdrawal Request Transaction in Strata's EVM. | ||
""" | ||
self.debug(f"EL address: {el_address}") | ||
self.debug(f"Bridge address: {PRECOMPILE_BRIDGEOUT_ADDRESS}") | ||
|
||
data_bytes = bytes.fromhex(change_address_pk) | ||
|
||
transaction = { | ||
"from": el_address, | ||
"to": PRECOMPILE_BRIDGEOUT_ADDRESS, | ||
"value": DEPOSIT_AMOUNT * SATS_TO_WEI, | ||
"gas": gas, | ||
"data": data_bytes, | ||
} | ||
l2_tx_hash = web3.eth.send_transaction(transaction) | ||
return l2_tx_hash | ||
|
||
def estimate_withdraw_gas( | ||
self, ctx: flexitest.RunContext, web3: Web3, el_address, change_address_pk | ||
): | ||
""" | ||
Estimate the gas for the withdrawal transaction. | ||
""" | ||
self.debug(f"EL address: {el_address}") | ||
self.debug(f"Bridge address: {PRECOMPILE_BRIDGEOUT_ADDRESS}") | ||
|
||
data_bytes = bytes.fromhex(change_address_pk) | ||
# Re-check duties | ||
duties = seqrpc.strata_getBridgeDuties(0, 0)["duties"] | ||
withdraw_duty = [d for d in duties if d["type"] == "FulfillWithdrawal"][0] | ||
new_assigned_op_idx = withdraw_duty["payload"]["assigned_operator_idx"] | ||
new_assigned_operator = ctx.get_service(f"bridge.{new_assigned_op_idx}") | ||
|
||
transaction = { | ||
"from": el_address, | ||
"to": PRECOMPILE_BRIDGEOUT_ADDRESS, | ||
"value": DEPOSIT_AMOUNT * SATS_TO_WEI, | ||
"data": data_bytes, | ||
} | ||
return web3.eth.estimate_gas(transaction) | ||
# Ensure a new operator is assigned | ||
assert new_assigned_operator != assigned_operator, "No new operator was assigned" | ||
|
||
def mine_blocks_until_maturity( | ||
self, | ||
btcrpc, | ||
withdraw_address, | ||
btc_url, | ||
btc_user, | ||
btc_password, | ||
original_balance, | ||
number_of_blocks=12, | ||
): | ||
""" | ||
Mine blocks until the withdraw address has a positive balance | ||
By default, the number of blocks to mine is 12: | ||
- 6 blocks to mature the DRT | ||
- 6 blocks to mature the DT | ||
""" | ||
btcrpc.proxy.generatetoaddress(number_of_blocks, UNSPENDABLE_ADDRESS) | ||
wait_until( | ||
lambda: get_balance(withdraw_address, btc_url, btc_user, btc_password) | ||
> original_balance | ||
) | ||
return True |
Oops, something went wrong.