diff --git a/functional-tests/envs/testenv.py b/functional-tests/envs/testenv.py index 0ec882c8a1..ecce977bdd 100644 --- a/functional-tests/envs/testenv.py +++ b/functional-tests/envs/testenv.py @@ -5,7 +5,6 @@ import flexitest from strata_utils import ( deposit_request_transaction, - extract_p2tr_pubkey, get_address, get_recovery_address, xonlypk_to_descriptor, @@ -107,31 +106,29 @@ def deposit(self, ctx: flexitest.RunContext, el_address, bridge_pk): error_with="Strata balance after deposit is not as expected", ) - def withdraw( + def withdraw_op_return( self, ctx: flexitest.RunContext, el_address: str, - withdraw_address: str, + payload: str, ): """ - Perform a withdrawal from the L2 to the given BTC withdraw address. + Perform a withdrawal from the L2 to the BTC using the OP_RETURN with the given payload. Returns (l2_tx_hash, tx_receipt, total_gas_used). """ cfg: RollupConfig = ctx.env.rollup_cfg() # D BTC deposit_amount = cfg.deposit_amount - # Build the p2tr pubkey from the withdraw address - change_address_pk = extract_p2tr_pubkey(withdraw_address) - self.debug(f"Change Address PK: {change_address_pk}") + self.debug(f"OP_RETURN payload: {payload}") # Estimate gas - estimated_withdraw_gas = self.__estimate_withdraw_gas( - deposit_amount, el_address, change_address_pk + estimated_withdraw_gas = self.__estimate_withdraw_gas_op_return( + deposit_amount, el_address, payload ) self.debug(f"Estimated withdraw gas: {estimated_withdraw_gas}") - l2_tx_hash = self.__make_withdraw( - deposit_amount, el_address, change_address_pk, estimated_withdraw_gas + l2_tx_hash = self.__make_withdraw_op_return( + deposit_amount, el_address, payload, estimated_withdraw_gas ).hex() self.debug(f"Sent withdrawal transaction with hash: {l2_tx_hash}") @@ -176,6 +173,27 @@ def __make_withdraw( l2_tx_hash = self.web3.eth.send_transaction(transaction) return l2_tx_hash + def __make_withdraw_op_return( + self, + deposit_amount, + el_address, + payload, + gas, + ): + """ + Withdrawal Request Transaction in Strata's EVM. + """ + + transaction = { + "from": el_address, + "to": PRECOMPILE_BRIDGEOUT_ADDRESS, + "value": deposit_amount * SATS_TO_WEI, + "gas": gas, + "data": payload, + } + l2_tx_hash = self.web3.eth.send_transaction(transaction) + return l2_tx_hash + def __estimate_withdraw_gas(self, deposit_amount, el_address, change_address_pk): """ Estimate the gas for the withdrawal transaction. @@ -191,6 +209,19 @@ def __estimate_withdraw_gas(self, deposit_amount, el_address, change_address_pk) } return self.web3.eth.estimate_gas(transaction) + def __estimate_withdraw_gas_op_return(self, deposit_amount, el_address, payload): + """ + Estimate the gas for the withdrawal transaction using an OP_RETURN + """ + + transaction = { + "from": el_address, + "to": PRECOMPILE_BRIDGEOUT_ADDRESS, + "value": deposit_amount * SATS_TO_WEI, + "data": payload, + } + return self.web3.eth.estimate_gas(transaction) + def make_drt(self, ctx: flexitest.RunContext, el_address, musig_bridge_pk): """ Deposit Request Transaction diff --git a/functional-tests/tests/bridge_withdraw_happy_op_return.py b/functional-tests/tests/bridge_withdraw_happy_op_return.py new file mode 100644 index 0000000000..9282147d67 --- /dev/null +++ b/functional-tests/tests/bridge_withdraw_happy_op_return.py @@ -0,0 +1,103 @@ +import flexitest +from bitcoinlib.services.bitcoind import BitcoindClient +from strata_utils import get_balance, string_to_opreturn_descriptor + +from envs import net_settings, testenv +from utils import generate_n_blocks, get_bridge_pubkey, wait_until + +# Local constants +# 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" + + +@flexitest.register +class BridgeWithdrawHappyOpReturnTest(testenv.BridgeTestBase): + """ + Makes two DRT deposits to the same EL address, then makes a withdrawal to an OP_RETURN. + + Checks if the balance of the EL address is expected + and if the BTC has an OP_RETURN block. + """ + + def __init__(self, ctx: flexitest.InitContext): + fast_batch_settings = net_settings.get_fast_batch_settings() + ctx.set_env( + testenv.BasicEnvConfig( + pre_generate_blocks=101, + rollup_settings=fast_batch_settings, + # need to manually control the block generations + auto_generate_blocks=False, + ) + ) + + def main(self, ctx: flexitest.RunContext): + btc = ctx.get_service("bitcoin") + seq = ctx.get_service("sequencer") + # create both btc and sequencer RPC + btcrpc: BitcoindClient = btc.create_rpc() + seqrpc = seq.create_rpc() + # generate 5 btc blocks + generate_n_blocks(btcrpc, 5) + + # Wait for seq + wait_until( + lambda: seqrpc.strata_protocolVersion() is not None, + error_with="Sequencer did not start on time", + ) + generate_n_blocks(btcrpc, 5) + + # Generate addresses + address = ctx.env.gen_ext_btc_address() + withdraw_address = ctx.env.gen_ext_btc_address() + el_address = self.eth_account.address + payload = "hello world" + bosd = string_to_opreturn_descriptor(payload) + self.debug(f"BOSD: {bosd}") + + self.debug(f"Address: {address}") + self.debug(f"Change Address: {withdraw_address}") + self.debug(f"EL Address: {el_address}") + + # Original BTC balance + btc_url = self.btcrpc.base_url + btc_user = self.btc.get_prop("rpc_user") + btc_password = self.btc.get_prop("rpc_password") + original_balance = get_balance(withdraw_address, btc_url, btc_user, btc_password) + self.debug(f"BTC balance before withdraw: {original_balance}") + + # Make sure starting ETH balance is 0 + check_initial_eth_balance(self.rethrpc, el_address, self.debug) + + bridge_pk = get_bridge_pubkey(self.seqrpc) + self.debug(f"Bridge pubkey: {bridge_pk}") + + # make two deposits + self.deposit(ctx, el_address, bridge_pk) + self.deposit(ctx, el_address, bridge_pk) + + # Withdraw + self.withdraw_op_return(ctx, el_address, bosd) + + # Move forward a single block + block = generate_n_blocks(btcrpc, 1)[0] # There's only one + last_block = btcrpc.getblock(block) + + # Get the output of the tx + # OP_RETURN is the second output + outputs = last_block["txs"][0].as_dict()["outputs"] + op_return_output = outputs[1] + self.debug(f"OP_RETURN output: {op_return_output}") + op_return_script_type = op_return_output["script_type"] + assert op_return_script_type == "nulldata", "OP_RETURN not found" + + return True + + +def check_initial_eth_balance(rethrpc, address, debug_fn=print): + """Asserts that the initial ETH balance for `address` is zero.""" + balance = int(rethrpc.eth_getBalance(address), 16) + debug_fn(f"Strata Balance before deposits: {balance}") + assert balance == 0, "Strata balance is not expected (should be zero initially)" diff --git a/functional-tests/utils/utils.py b/functional-tests/utils/utils.py index 07b29ab9de..1e61df5e9c 100644 --- a/functional-tests/utils/utils.py +++ b/functional-tests/utils/utils.py @@ -51,6 +51,7 @@ def generate_n_blocks(bitcoin_rpc: BitcoindClient, n: int): try: blk = bitcoin_rpc.proxy.generatetoaddress(n, addr) print(f"made blocks {blk}") + return blk except Exception as ex: logging.warning(f"{ex} while generating address") return @@ -156,9 +157,9 @@ def check_nth_checkpoint_finalized( timeout=3, ) - assert ( - syncstat["finalized_block_id"] != batch_info["l2_blockid"] - ), "Checkpoint block should not yet finalize" + assert syncstat["finalized_block_id"] != batch_info["l2_blockid"], ( + "Checkpoint block should not yet finalize" + ) assert batch_info["idx"] == idx checkpoint_info_next = seqrpc.strata_getCheckpointInfo(idx + 1) assert checkpoint_info_next is None, f"There should be no checkpoint info for {idx + 1} index"