Golden Gate — Trustless-Bridging Ethereum (EVM) Blockchains — Part 1: Basics

A Symmetrical Light Client

A Symmetrical Prover

  • get the Merkle proof data from a chain B full node (archive node if a storage proof is needed)
  • send the proof data as calldata to the Prover contract on chain A, along with the header corresponding to the chain B block containing the state change we want to verify
  • the Prover contract computes the block hash from the header data and checks whether it is a valid hash, by querying the Light Client smart contract on chain A, which keeps track of chain B block hashes
  • proof data is verified against the bytes32 Patricia-Merkle trie root found in the block header
struct BlockHeader {
bytes32 parentHash;
bytes32 sha3Uncles;
address miner;
bytes32 stateRoot;
bytes32 transactionsRoot;
bytes32 receiptsRoot;
bytes logsBloom;
uint256 difficulty;
uint256 number;
uint256 gasLimit;
uint256 gasUsed;
uint256 timestamp;
bytes extraData;
bytes32 mixHash;
uint64 nonce;
uint256 totalDifficulty;
}
function verifyHeader(
EthereumDecoder.BlockHeader memory header
)
view public
returns (bool valid, string memory reason)
{
bytes32 blockHash = keccak256(getBlockRlpData(header));
if (blockHash != header.hash) {
return (false, "Header data or hash invalid");
}
// Check block hash was registered in light client
bytes32 blockHashClient = client.getConfirmedBlockHash(header.number);
if (blockHashClient != header.hash) {
return (false, "Unregistered block hash");
}
return (true, "");
}
{
receiptIndex: 0,
headerData: '0xf90211a0460c84f2877fd351416cc9207bbb140eda2a59f0501bda5d4e814e017024536fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948595dd9e0438640b5e1254f9df579ac12a86865fa0c2377ee7587692810e987464064b2247c8d7d6ad0f9fce327bd27ad0c92a62afa0a4041a7e77fc216726e5015e6712d6434a1a4991781ed9478f5451ad5ac1e362a077ee012c7ec8a7a5cc6f8c7899e4176f8716ce11c637820d84a09e2b548191d3b90100000000000000001000000000000000000000000000100000000000000000000000000040000000000000000000400100000000000000000000000000800000000000000000000000000020080000040000000000000000000000000000100000000000000000400000080000000000000000000000000000000000100000000000000000000000000000000000004000000000000000000000020000001000000000000000000000000000800000000000000000000000000000000004000000000000020000280000000000000800000500000100000000400000200000000000800000000000000000000000000000020000000000000000000000040000009032323137373831393836303031333938839a4aa383979f3e83024c39845ec6f2f487657a696c2e6d65a0f0a64ca3c939e2506d05505449b4caaa06eba7c86062997e6632b0ee3db977df88e284f1c803fa6f5a',
receiptData: '0xf901a60182574ab9010000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000080000000000000000000000000002008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000010000000000000000000020000000000080000000000000000000000000000000000000000000000000000004000000f89df89b94d26114cd6ee289accf82350c8d8487fedb8a0c07f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000002c7116a63ab91084a7a5d6fef2e4eda0c84487afa00000000000000000000000007d3cd5685188c6aa498697db91ca548a1249863ea0000000000000000000000000000000000000000000000001158e460913d00000',
logEntry: '0xf89b94d26114cd6ee289accf82350c8d8487fedb8a0c07f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000002c7116a63ab91084a7a5d6fef2e4eda0c84487afa00000000000000000000000007d3cd5685188c6aa498697db91ca548a1249863ea0000000000000000000000000000000000000000000000001158e460913d00000',
proof: [
[
"0x6adc4881ae9f2b2bbbf70a60e5b05f0734c02d731e80ac1503231d851b24ffe6",
"0x", "0x", "0x", "0x", "0x", "0x", "0x",
"0x103165b38cd8ad3ffa4b1de70e7391ac2c321ffe265bc77f2316ba33288c3717",
"0x", "0x", "0x", "0x", "0x", "0x", "0x", "0x",
],
[
"0x30",
"0xf901a60182574ab9010000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000080000000000000000000000000002008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000010000000000000000000020000000000080000000000000000000000000000000000000000000000000000004000000f89df89b94d26114cd6ee289accf82350c8d8487fedb8a0c07f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000002c7116a63ab91084a7a5d6fef2e4eda0c84487afa00000000000000000000000007d3cd5685188c6aa498697db91ca548a1249863ea0000000000000000000000000000000000000000000000001158e460913d00000",
]
]
}
struct TransactionReceipt {
uint8 status;
uint256 gasUsed;
bytes logsBloom;
Log[] logs;
}
struct Log {
address contractAddress;
bytes32[] topics;
bytes data;
}

Proving logs happened

  • get the receipt proof for the receipt containing the log and the header for the block in which it was mined.
  • check the header as shown above
  • check the receipt proof
function verifyLog(
MPT.MerkleProof memory receiptdata,
bytes memory logdata,
uint256 logIndex
)
pure public override
returns (bool valid, string memory reason)
{
EthereumDecoder.TransactionReceiptTrie memory receipt =
EthereumDecoder.toReceipt(receiptdata.expectedValue);
if (keccak256(logdata) == keccak256(EthereumDecoder.getLog(receipt.logs[logIndex]))) {
return (true, "");
}
return (false, "Log not found");
}

Log-based proving systems

// Chain A
user -> TokenChainA: lockTokens()
TokenChainA -> TokenChainA: TokenLocked event
// Chain B
user -> TokenChainB: header, receipt proof, expected log
TokenChainB -> ProverChainB: verifyHeaderReceiptAndLog() ProverChainB -> LightClient: getConfirmedBlockHash()
LightClient -> ProverChainB: hashIsValid (bool)
ProverChainB -> TokenChainB: proofIsValid (bool)
TokenChainB -> TokenChainB: mintTokens()
TokenChainB -> TokenChainB: TokenMinted event
// Chain A
user -> TokenChainA: lockTokens()
TokenChainA -> TokenChainA: TokenLocked event
// Chain B
user -> ProverChainB: header, receipt proof, expected log
ProverChainB -> ProverChainB: verifyHeader()
ProverChainB -> LightClient: getConfirmedBlockHash()
LightClient -> ProverChainB: hashIsValid (bool)
ProverChainB -> ProverChainB: verifyReceipt()
ProverChainB -> ProverChainB: verifyLog()
ProverChainB -> TokenChainB: mintTokens()
TokenChainB -> TokenChainB: TokenMinted event

Proving transaction existence and outcome

struct Transaction {
uint256 nonce;
uint256 gasPrice;
uint256 gasLimit;
address to;
uint256 value;
bytes data;
uint8 v;
bytes32 r;
bytes32 s;
}
  • get the transaction proof and the header for the block in which it was mined.
  • check the block header as shown in the first part
  • check the transaction proof
function verifyTransaction(
EthereumDecoder.BlockHeader memory header,
MPT.MerkleProof memory txdata
)
pure public override
returns (bool valid, string memory reason)
{
if (header.transactionsRoot != txdata.expectedRoot) {
return (false, "verifyTransaction - different trie roots");
}
valid = txdata.verifyTrieProof();
if (!valid) {
return (false, "verifyTransaction - invalid proof");
}
return (true, "");
}
  • get receipt proof for the receipt with the same index as the transaction we want to prove
  • verify receipt against the header’s receiptsRoot
  • RLP decode receipt data and check the value of receipt.status

Proving state after a block is processed

struct Account {
uint256 nonce;
uint256 balance;
bytes32 storageRoot;
bytes32 codeHash;
}
  • get the account proof and the header for the block of interest.
  • check the block header validity as shown in the first part
  • check the account proof
function verifyAccount(
EthereumDecoder.BlockHeader memory header,
MPT.MerkleProof memory accountdata
)
pure public override
returns (bool valid, string memory reason)
{
if (header.stateRoot != accountdata.expectedRoot) {
return (false, "verifyAccount - different trie roots");
}
valid = accountdata.verifyTrieProof();
if (!valid) {
return (false, "verifyAccount - invalid proof");
}
return (true, "");
}
  • decode the RLP-encoded account data and extract the storageRoot
  • check the storage proof against the storageRoot
function verifyStorage(
MPT.MerkleProof memory accountProof,
MPT.MerkleProof memory storageProof
)
pure public override
returns (bool valid, string memory reason)
{
EthereumDecoder.Account memory account = EthereumDecoder.toAccount(accountProof.expectedValue);
if (account.storageRoot != storageProof.expectedRoot) {
return (false, "verifyStorage - different trie roots");
}
valid = storageProof.verifyTrieProof();
if (!valid) {
return (false, "verifyStorage - invalid proof");
}
return (true, "");
}

Basics done. Now what?

This is not all!

--

--

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