Golden Gate — Trustless-Bridging Ethereum (EVM) Blockchains — Part 2: Transaction Replay

Image for post
Image for post

Read it on Github with syntax highlighting.

In Trustless-Bridging Ethereum (EVM) Blockchains — Part 1: Basics you saw that we have the tools to prove any momentary state change that happened on chain A, on another chain B, assuming that chain B has access to trustworthy chain A block hashes.

Today we will explore a mechanism for keeping smart contract state in sync between two EVM-based chains, as demoed in the following video:

Protocols usually implement replay protections to make sure malicious actors cannot execute state-changing transaction crafted on another chain. But purposefully implementing a mechanism for replaying state changes or syncing state snapshots directly, can be a useful pattern for keeping assets on multiple chains or migrating from one chain to another.

What does it mean for a state change to be replayed? Using the same signed transaction on chain B, that a user sent on chain A.

struct Transaction {
uint256 nonce;
uint256 gasPrice;
uint256 gasLimit;
address to;
uint256 value;
bytes data;
uint8 v;
bytes32 r;
bytes32 s;
}

A successful replay requires the same setup on both chains:

Therefore, to create a protocol that allows replaying transactions for the purpose of having a correctly synced state between chains, we need a Proxy contract that fulfills the role of the miner: validates the transaction and runs it.

The above rules, allow us to reuse the exact same calldata on all chains where we want to have synced state changes. The Proxy contract will keep track of the account nonce per each chain ID that it supports and will forward the call to the appropriate contract address ( to field), with some caveats for this receiving contract:

Image for post
Image for post

What are the steps for replaying a transaction on another chain?

function forwardAndVerify (
EthereumDecoder.BlockHeader memory header,
MPT.MerkleProof memory accountdata,
MPT.MerkleProof memory txdata,
MPT.MerkleProof memory receiptdata,
uint256 chainId
) public returns (bytes memory);
(bool valid, string memory reason) = verifyHeader(header);
if (!valid) revert(reason);
(valid, reason) = verifyTransaction(header, txdata);
if (!valid) revert(reason);
(valid, reason) = verifyReceipt(header, receiptdata);
if (!valid) revert(reason);
(valid, reason) = verifyAccount(header, accountdata);
if (!valid) revert(reason);
require(
keccak256(txdata.key) == keccak256(receiptdata.key),
"Transaction & receipt index must be the same."
);
Account memory account = toAccount(
accountdata.expectedValue
);
Transaction memory transaction = toTransaction(
txdata.expectedValue
);
TransactionReceipt memory receipt = toReceipt(
receiptdata.expectedValue
);
address sender = getTransactionSender(txdata, chainId);
if (accountNonces[sender] > 0) {
require(
account.nonce == accountNonces[sender] + 1,
"Account nonce out of sync"
);
}
accountNonces[sender] = account.nonce;
bytes32 codeHash;
address target = transaction.to;
assembly {
codeHash := extcodehash(target)
}
require(account.codeHash == codeHash);
(bool success, bytes memory data) = transaction.to.call{
value: transaction.value,
gas: transaction.gasLimit
}(transaction.data);
uint8 _success = success ? uint8(1) : uint8(0);
require(
_success == receipt.status,
"Diverged transaction status"
);
return data;

Transaction hygiene

For this system to work properly, it requires transaction hygiene. Nonces need to be kept in sync across chains and this means that cross-chain accounts need to use an account manager to send transactions and make sure they are not sent out of order.

Cross-chain synced contracts need to be deployed with the same address, which can easily be achieved by deploying them from the same account address, at the same nonce.

The EVM does not give access to transaction data (e.g. transaction hash) or the value of the account’s nonce for that transaction. If there was such access, the need for storing the nonce on-chain (yet again) would not exist.

Shadow payments

This transaction replay pattern can be used for shadow payments.

Projects can maintain their own rich-interaction chains, only for their users, shadowing the contracts on the high-value chains that handle more important payments or transactions.

For a bridge between something that does not have an EVM-like execution environment and something that has, this could add additional behavior to a purchase/transaction.

This bridge may be extended to Bitcoin, so this becomes especially relevant — you could do something on Ethereum dependent upon buying something with Bitcoin.

Other mechanisms

We can do a mechanism for syncing state changes without these restrictions of using the same sender account. But it does not get easier, we would just need other rules to ensure the protocol cannot be gamed: a pattern of executing correlated actions, which we will discuss in the next articles. You might be familiar with this pattern because it is widely used for locking tokens on one chain and minting them on another chain.

We can also keep the storage state in sync — entirely or sparsely, depending on what storage keys we need for each contract. Then, we do not replay state changes, but sync snapshots of data at certain points in time. We will talk about the rules of this mechanism in the next articles.

Originally published at https://github.com.

Written by

Building bricks for the World Computer #ethereum #Pipeline #dType #EIP1900 https://github.com/loredanacirstea, https://www.youtube.com/c/LoredanaCirstea

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store