This document defines the fraud proofs used to audit block execution on the chain peg contract. While these are referred to abstractly as "fraud proofs" they are really error proofs, as they do not inherently indicate intent to defraud and could be the result of processing errors. As such, we use the term fraud proofs generally in keeping with common practice, but individual proofs are referred to as error proofs.
A number of utility functions are defined to simplify code which is regularly reused in the other libraries.
Verifies that an encoded state proof is a valid merkle proof of an account's inclusion in the provided state root and returns the account struct, the index of the account in the tree, the merkle proof (which is used for updates, where necessary) and whether the account was empty.
Inputs
stateRoot
- Root hash of the state tree.stateProof
- EncodedStateProof
struct.
Process
- Decodes
stateProof
as aStateProof
struct with fields(bytes data, uint256 accountIndex, bytes32[] siblings)
- Verifies the merkle proof with
verifyMerkleProof(stateRoot, data, accountIndex, siblings)
- Decodes
data
as an account- If
data
is a null buffer of 32 bytes, creates a default account struct and sets a booleanempty
to true - Otherwise, decodes it as an account struct
- If
- Returns
(empty, accountIndex, siblings, account)
Verifies the state root prior to a transaction.
Inputs
previousRootProof
- ABI encoded form of either a block header or a transaction inclusion proofblockHeader
- Block header of the block with the original transactiontransactionIndex
- Index of the original transaction
Process
-
If
transactionIndex == 0
, decodespreviousRootProof
as a block header and verifies that its block number is one less thanblockHeader
and that it exists in the set of submitted blocks, then returnsblockHeader.stateRoot
. -
Otherwise, decodes
previousRootProof
as(bytes, bytes32[])
and verifies it as a transaction inclusion proof inblockHeader.transactionsRoot
usingtransactionIndex - 1
as the index, then returns the last 32 bytes of the decodedbytes
as the previous state root.
Verifies the state of an account in the state root prior to a transaction.
Inputs
badHeader
- Header of the block with the original transactiontransactionIndex
- Index of the original transactionpreviousRootProof
- ABI encoded form of either a block header or a transaction inclusion proofstateProof
- Merkle proof of the account in the previous state root.
Process
- Verify the previous state proof using
transactionHadPreviousState(previousStateProof, badHeader, transactionIndex)
and sets the result aspreviousRoot
. - Return the result of
verifyAccountInState(previousRoot, stateProof)
Decodes and validates a TransactionStateProof, which contains an inclusion proof for a transaction and the state root prior to its execution.
Inputs
header
- Header of the block with the original transactionproofBytes
- encodedTransactionStateProof
which has fields(uint256 transactionIndex, bytes32[] siblings, bytes previousRootProof)
transactionBytes
- encoded transaction to verify inclusion proof of
Note: This struct is used because of some issues we ran into with ABIEncoderV2 when the params were provided as calldata.
Process
- Decode
proofBytes
as(uint256 transactionIndex, bytes32[] siblings, bytes previousRootProof)
- Verify that
header
is a pending block - Verify the merkle proof using
verifyMerkeProof(header.transactionsRoot, transactionBytes, trasactionIndex, siblings)
- Return the result of
transactionHadPreviousState(previousRootProof, header, transactionIndex)
Block error proofs are used to demonstrate that either the transactions buffer or the block header in a block were invalid in some way.
Proves that a block had an invalid stateRoot
field by proving that it does not match the intermediateStateRoot
of its last transaction.
Inputs
header
- Header of the blocktransactionsData
- Transactions buffer from the block
Process
- Verify that the
header
is a pending block - Verify that
header.transactionsHash == keccak256(transactionsData)
- Take the last 32 bytes of
transactionsData
asexpectedRoot
- If
expectedRoot != header.stateRoot
revert the block.
Proves that a block had an invalid stateSize
field by proving that it does not correctly increment the previous block's stateSize
Inputs
parent
- Header of the previous blockheader
- Header of the blocktransactionsData
- Transactions buffer from the block
Process
- Verify that the
header
is a pending block - Verify that
header.blockNumber == parent.blockNumber + 1
- Verify that
parent
is a block that has been recorded, either pending or confirmed - Decode
TransactionsMetadata
from the first 16 bytes oftransactionsData
- Sum
meta.hardCreateCount
andmeta.softCreateCount
astotalCreates
- If
parent.stateSize + totalCreates != header.stateSize
revert the block.
Proves that the hardTransactionsCount
in the block header is not equal to the total number of hard transactions in the metadata plus the previous block's hard transactions count.
Inputs
parent
- Header of the previous blockheader
- Header of the blocktransactionsData
- Transactions buffer from the block
Process
- Verify that the
header
is a pending block - Verify that
header.blockNumber == parent.blockNumber + 1
- Verify that
parent
is a block that has been recorded, either pending or confirmed - Verify that
header.transactionsHash == keccak256(transactionsData)
- Decode
TransactionsMetadata
from the first 16 bytes oftransactionsData
- Set
meta.hardCreateCount + meta.hardDepositCount + meta.hardWithdrawCount + meta.hardAddSignerCount
tototalHardTransactions
- If
parent.hardTransactionsCount + totalHardTransactions != header.hardTransactionsCount
revert the block.
Proves that a block has a missing or duplicate hard transaction index.
Inputs
parent
- Header of the previous blockheader
- Header of the blocktransactionsData
- Transactions buffer from the block
Process
- Verify that the
header
is a pending block - Verify that
header.blockNumber == parent.blockNumber + 1
- Verify that
parent
is a block that has been recorded, either pending or confirmed - Verify that
header.transactionsHash == keccak256(transactionsData)
- Decode
TransactionsMetadata
from the first 16 bytes oftransactionsData
- Calculate
totalHardTransactions
as:
(
meta.hardCreateCount + meta.hardDepositCount +
meta.hardWithdrawCount + meta.hardAddSignerCount
)
- Set
previousTotal
toparent.hardTransactionsCount
- Set
fraudProven = false
- Create variable
bytes buffer = new bytes(totalHardTransactions)
- Set
txPtr
to the memory location oftransactionsData
plus 48 (to skip the length field that Solidity sets and the metadata) - Set
bufferPtr
to the memory location ofbuffer
plus 32 (to skip the length field that Solidity sets) - Loop through each hard transaction type:
- If
fraudProven
, skip - Set
length
to the appropriate length field for the transaction type in the metadata - Loop from
0
tolength
- Read
hardTransactionIndex
from the first 5 bytes aftertxPtr
- Set
relativeIndex = hardTransactionIndex - previousTotal
- Read a single byte from
bufferPtr + relativeIndex
- If the byte is not zero, set
fraudProven = true
and break the loop - Set the byte to 1
- Increment
txPtr
by the length of the transaction type
- Read
- If
fraudProven == true
revert the block.
Proves that a block has a hard transaction which is out of order.
Inputs
header
- Header of the blocktransactionsData
- Transactions buffer from the block
Process
- Verify that the
header
is a pending block - Verify that
header.transactionsHash == keccak256(transactionsData)
- Decode
TransactionsMetadata
from the first 16 bytes oftransactionsData
- Set
fraudProven = false
- Set
txPtr
to the memory location oftransactionsData
plus 48 (to skip the length field that Solidity sets and the metadata) - Loop through each hard transaction type:
- If
fraudProven
, skip - Set
length
to the appropriate length field for the transaction type in the metadata - Set
last = 0
- Loop from
0
tolength
:- Read
hardTransactionIndex
from the first 5 bytes aftertxPtr
- If
(hardTransactionIndex <= last)
setfraudProven = true
and break the loop - Set
last = hardTransactionIndex
- Increment
txPtr
by the length of the transaction type
- Read
- If
- If
fraudProven == true
revert the block.
Proves that the length of the transactions data in a block is invalid. "invalid" means that it either did not contain the transaction metadata or that the length is not consistent with the length expected from the metadata.
Inputs
header
- Header of the blocktransactionsData
- Transactions buffer from the block
Process
- Verify that the
header
is a pending block - Verify that
header.transactionsHash == keccak256(transactionsData)
- If
transactionsData.length < 16
revert the block and cease execution. - Decode
TransactionsMetadata
from the first 16 bytes oftransactionsData
- Calculate
expectedLength
as:
16 +
(meta.hardCreateCount * 88 +
meta.hardDepositCount * 48 +
meta.hardWithdrawCount * 68 +
meta.hardAddSignerCount * 61 +
meta.softWithdrawCount * 131 +
meta.softCreateCount * 155 +
meta.softTransferCount * 115 +
meta.softChangeSignerCount * 125);
- If
transactionsData.length != expectedLength
revert the block.
Transaction errors are used to prove that a hard transaction does not match its source or that a soft transaction has an invalid signature.
Proves that a hard transaction in a block does not match the original hard transaction recorded on mainnet.
Input
header
- header of the block with the errortransaction
- encoded transactiontransactionIndex
- index of the transactionsiblings
- merkle proof of the transactionpreviousRootProof
(optional) - ABI encoded proof of the previous state root for the transaction, only used forHardAddSigner
andHardDeposit
typesstateProof
(optional) - encodedStateProof
, only used forHardAddSigner
andHardDeposit
types
Process
- Verify that the
header
is a pending block - Read the first byte from
transaction
asprefix
- Verify that
prefix < 4
- Verify that the merkle proof is valid with
verifyMerkleProof(header.transactionsRoot, transaction, transactionIndex, siblings)
- Read
hardTransactionIndex
from bytes 1-6 intransaction
- Retrieve the original hard transaction from the recorded hard transactions on the peg contract as
originalTransaction
- Use the appropriate function listed below to handle the rest of the verification. The function will return a boolean for whether an error was found.
- If the result is true, revert the block.
Input
inputData
- original hard transaction from the peg contractoutputData
- actual hard transaction in the block
Process
- If
outputData.length != 89
or the first byte ofinputData
is not0
, returntrue
- Decode
inputData
as an input hard deposit, with fields(address contractAddress, address signerAddress, uint56 value)
and set it toinput
- Decode
outputData
as an output hard create with fieldsuint40 hardTransactionIndex, uint32 accountIndex, uint56 value, address contractAddress, address signerAddress, bytes32 intermediateStateRoot
and set it tooutput
- If any of the following checks fail, return
true
:output.contractAddress == input.contractAddress
output.signerAddress == input.signerAddress
output.value == input.value
Input
header
- block header with the transactioninputData
- original hard transaction from the peg contractoutputData
- actual hard transaction in the blocktransactionIndex
- index of the transactionpreviousRootProof
(optional) - ABI encoded proof of the previous state root for the transactionstateProof
(optional) - encodedStateProof
Process
- If
outputData.length != 49
or the first byte ofinputData
is not equal to0
, returntrue
- Decode
inputData
as an input hard deposit, with fields(address contractAddress, address signerAddress, uint56 value)
and set it toinput
- Decode
outputData
as an output hard deposit with fieldsuint40 hardTransactionIndex, uint32 accountIndex, uint56 value, bytes32 intermediateStateRoot
and set it tooutput
- If
input.value != output.value
returntrue
- Set
previousRoot
to the result of callingtransactionHadPreviousState(previousRootProof, header, transactionIndex)
- Get the return values
accountIndex, account
from the result ofverifyAccountInState(previousRoot, stateProof)
- Set
indexMatch
tooutput.accountIndex == accountIndex
- Set
addressMatch
toaccount.contractAddress == input.contractAddress
- If
indexMatch
is true andaddressMatch
is false, orindexMatch
is false andaddressMatch
is true, the caller has proved that the deposit was given to the wrong account. Returntrue
. - Otherwise return false.
Input
inputData
- original hard transaction from the peg contractoutputData
- actual hard transaction in the block
Process
- If
outputData.length != 49
or the first byte ofinputData
is not2
return true. - Decode
inputData
as an input hard withdrawal, with fields(uint32 accountIndex, address caller, uint56 value)
and set it toinput
- Decode
outputData
as an output hard withdrawal with fieldsuint40 hardTransactionIndex, uint32 accountIndex, address withdrawalAddress, uint56 value, bytes32 intermediateStateRoot
and set it tooutput
- If any of the following checks fail, return
true
:- input.accountIndex == output.accountIndex
- input.value == output.value
- input.caller == output.withdrawalAddress
- Otherwise return
false
Inputs
header
- block header with the transactioninputData
- original hard transaction from the peg contractoutputData
- actual hard transaction in the blocktransactionIndex
- index of the transactionpreviousRootProof
(optional) - ABI encoded proof of the previous state root for the transactionstateProof
(optional) - encodedStateProof
Process
- If
outputData.length != 94
or the first byte ofinputData
is not equal to3
, returntrue
- Decode
inputData
as an input hard add signer with fields(uint32 accountIndex, address caller, address signingAddress)
- Decode
outputData
as an output hard add signer with fields(uint40 hardTransactionIndex, uint32 accountIndex, address signingAddress, bytes32 intermediateStateRoot)
- If
input.accountIndex != output.accountIndex
orinput.signingAddress != output.signingAddress
are true, returntrue
- Set
previousRoot
to the result of callingtransactionHadPreviousState(previousRootProof, header, transactionIndex)
- Get the return values
accountIndex, account
from the result ofverifyAccountInState(previousRoot, stateProof)
- If
accountIndex != input.accountIndex
revert the transaction. - If
output.intermediateStateRoot != previousRoot
andinput.caller != account.contractAddress
returntrue
- Otherwise return
false
Proves that a transaction had an invalid signature or a signature from an account not in the signers array.
Input
header
- header of the block with the bad transactiontransaction
- transaction with the bad signaturetransactionIndex
- index of the transactionsiblings
- merkle proof of the transactionpreviousRootProof
- encodedTransactionProof
stateProof
- encodedStateProof
Process
- Verify that the
header
is a pending block - Read the first byte from
transaction
asprefix
- Verify that
prefix > 3
- Verify that the merkle proof is valid with
verifyMerkleProof(header.transactionsRoot, transaction, transactionIndex, siblings)
- Get the signer address by calling
recoverSignature(transaction)
- If the address is equal to zero, it had an invalid signature, revert the block and stop execution.
- Get
previousRoot
by callingtransactionHadPreviousState(previousRootProof, header, transactionIndex)
- Get
(accountIndex, account)
fromverifyAccountInState(previousRoot, stateProof)
- Get the transaction's account index from bytes 4-8
- Verify that the transaction's account index is equal to
accountIndex
, otherwise revert - Check if
account.signers
contains the signer address from step 5. - If it does not, revert the block.
Execution errors are used to prove that a transaction was executed incorrectly, either because the state of the chain prior to the transaction does not allow the transaction to be executed, or because one or more output fields in the transaction are invalid.
Proves that a create transaction assigned an invalid account index to the new account.
Input
parent
- header of the previous blockheader
- header of the block with the errortransactionsData
- transaction buffer from the blocktransactionsIndex
- index of the transaction with the bad account index
Process
- Verify that
header
is a pending block and thatparent
is a pending or confirmed block with a block number one less thanheader
's - Decode the transactions metadata from
transactionsData
asmeta
- If
transactionsIndex
is less thanmeta.hardCreatesCount
:- Get the pointer to the beginning of the transaction as
transactionsData+48 + 88 * (meta.hardCreatesCount - transactionIndex)
- Read the account index from the buffer as the first 4 bytes after
pointer + 5
- If the account index is not equal to
parent.stateSize + (meta.hardCreatesCount - transactionIndex)
revert the block
- Get the pointer to the beginning of the transaction as
- Otherwise:
- Calculate the sum of previous transaction counts as
priorSum
for the set of hard transactions and soft withdrawals. - Calculate the pointer by adding
transactionsData+48
to the sum of products of the count for each of aforementioned transaction types by its respective size. - Get the number of previously executed soft creates as
(meta.softCreatesCount - (transactionIndex - priorSum))
and sum it withmeta.hardCreatesCount
andparent.stateSize
. Set this toexpectedIndex
- Read the account index as the first 3 bytes after
pointer + 3
- If
expectedIndex != accountIndex
revert the block
- Calculate the sum of previous transaction counts as
Input
header
- header of the block with the errortransactionProof
- encodedTransactionStateProof
transaction
- encoded transaction with the errorstateProof1
- encodedStateProof
stateProof2
(optional) - encodedStateProof
Process
- Get
previousRoot
as the result of callingvalidateTransactionStateProof(header, transactionProof, transaction)
- Read the transaction prefix from the first byte.
- Use the prefix to decode
transaction
as the appropriate type and call the appropriate function listed below. - If the call does not cause the transaction to revert, revert the block.
Prove any of the following:
- the transaction was not executed
- an account already existed in the state with the transaction's contract address
- the account created was not empty before the transaction
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionstateProof
- encodedStateProof
transaction
-HardCreate
struct
Process
- Get the return values
(empty, accountIndex, siblings, provenAccount)
fromverifyAccountInState(previousRoot, stateProof)
- Hard creates can not be rejected, so if
transaction.intermediateStateRoot == previousRoot
, return - If the proven account index is not equal to the transaction's account index:
- Verify that the proven account has a contract address equal to the transaction's.
- If it does, return true, otherwise revert the transaction.
- If the proven account is not empty, return
- Create a new account with
balance = transaction.value, signers = [transaction.signerAddress], contractAddress = transaction.contractAddress
- Calculate the new state root with
updateAccount(newAccount, accountIndex, siblings)
- Return the transaction if the new state root is equal to the transaction's intermediate state root.
Prove any of the following:
- the transaction was not executed
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionstateProof
- encodedStateProof
transaction
-HardDeposit
struct
Process
- Get the return values
(empty, accountIndex, siblings, account)
fromverifyAccountInState(previousRoot, stateProof)
- Hard deposits can not be rejected, so if
transaction.intermediateStateRoot == previousRoot
, return - Verify that
transaction.accountIndex == accountIndex
(revert the transaction otherwise) - Increase
account.balance
bytransaction.value
- Recalculate the state root with
updateAccount(account, accountIndex, siblings)
- Revert the transaction if the new state root is equal to the transaction's intermediate state root.
Prove any of the following:
- the transaction was rejected and should not have been
- the transaction was not rejected and should have been
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionstateProof
- encodedStateProof
transaction
-HardWithdrawal
struct
Process
- Get the return values
(empty, accountIndex, siblings, account)
fromverifyAccountInState(previousRoot, stateProof)
- Verify that
accountIndex == transaction.accountIndex
, revert the transaction otherwise - Set
rejected
totransaction.intermediateStateRoot == previousRoot
- Set
shouldReject=true
if any of the following are true:empty == true
account.balance < transaction.value
transaction.withdrawalAddress != account.contractAddress
(withdrawal address is set to the caller address from when the transaction was recorded)
- If
shouldReject != rejected
return - If
rejected
is true, revert the transaction - Subtract
transaction.value
fromaccount.balance
- Recalculate the state root with with
updateAccount(account, accountIndex, siblings)
- Revert the transaction if the new state root is equal to the transaction's intermediate state root.
Prove any of the following:
- the transaction was rejected and should not have been
- the transaction was not rejected and should have been
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionstateProof
- encodedStateProof
transaction
-HardAddSigner
struct
Process
- Get the return values
(empty, accountIndex, siblings, account)
fromverifyAccountInState(previousRoot, stateProof)
- Verify that
accountIndex == transaction.accountIndex
, revert the transaction otherwise - Set
rejected
totransaction.intermediateStateRoot == previousRoot
- Set
shouldReject = true
if any of the following are true:empty == true
account.signers.length == 10
hasSigner(account, transaction.signerAddress) == true
- If
shouldReject != rejected
return - If
rejected
is true, revert the transaction. - Call
addSigner(account, transaction, signerAddress)
- Recalculate the state root with with
updateAccount(account, accountIndex, siblings)
- Revert the transaction if the new state root is equal to the transaction's intermediate state root.
Prove any of the following:
- the transaction was included despite having invalid preconditions
- the transaction nonce was not equal to the account's nonce
- the account had an insufficient balance
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionstateProof
- encodedStateProof
transaction
-SoftWithdrawal
struct
Process
- Get the return values
(empty, accountIndex, siblings, account)
fromverifyAccountInState(previousRoot, stateProof)
- Verify that
accountIndex == transaction.accountIndex
, revert the transaction otherwise - If either of the following are true, return:
account.balance < transaction.value
account.nonce != transaction.nonce
- Set
account.nonce += 1
- Set
account.balance -= transaction.value
- Recalculate the state root with with
updateAccount(account, accountIndex, siblings)
- Revert the transaction if the new state root is equal to the transaction's intermediate state root.
Prove any of the following:
- the transaction was included despite having invalid preconditions
- the sender had an insufficient balance
- the transaction nonce did not match the sender's nonce
- the contract address for the new account already existed in the state
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionsenderProof
- encodedStateProof
receiverProof
- encodedStateProof
transaction
-SoftCreate
struct
Process
- Get the return values
(senderIndex, senderSiblings, sender)
fromverifyAccountInState(previousRoot, senderProof)
- Verify
senderIndex == transaction.fromIndex
, revert otherwise - If either of the following are true, return:
sender.nonce != transaction.nonce
sender.value < transaction.value
- Set
sender.balance -= transaction.value
- Set
sender.nonce += 1
- Recalculate the state root with
updateAccount(sender, senderIndex, senderSiblings)
and set it tointermediateRoot
- Get the return values
(receiverEmpty, receiverIndex, receiverSiblings, receiver)
fromverifyAccountInState(intermediateRoot, receiverProof)
- If
receiverIndex != transaction.toIndex
:- Verify that
receiver.contractAddress == transaction.contractAddress
- If true, return
- If false, revert the transaction
- Verify that
- If
receiverEmpty
is false, return - Create a new account with
balance = transaction.value, signers = [transaction.signerAddress], contractAddress = transaction.contractAddress
- Recalculate the state root with
updateAccount(receiver, receiverIndex, receiverSiblings)
- If the new root is equal to the transaction's intermediate state root, revert the transaction.
Prove any of the following:
- the transaction was included despite having invalid preconditions
- the sender had an insufficient balance
- the transaction nonce did not match the sender's nonce
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionsenderProof
- encodedStateProof
receiverProof
- encodedStateProof
transaction
-SoftTransfer
struct
Process
- Get the return values
(senderIndex, senderSiblings, sender)
fromverifyAccountInState(previousRoot, senderProof)
- Verify
senderIndex == transaction.fromIndex
, revert otherwise - If either of the following are true, return:
sender.nonce != transaction.nonce
sender.value < transaction.value
- Set
sender.balance -= transaction.value
- Set
sender.nonce += 1
- Recalculate the state root with
updateAccount(sender, senderIndex, senderSiblings)
and set it tointermediateRoot
- Get the return values
(receiverIndex, receiverSiblings, receiver)
fromverifyAccountInState(intermediateRoot, receiverProof)
- Verify that
receiverIndex == transaction.toIndex
, otherwise revert the transaction. - Set
receiver.balance += transaction.value
- Recalculate the state root with
updateAccount(receiver, receiverIndex, receiverSiblings)
- If the new root is equal to the transaction's intermediate state root, revert the transaction.
Prove any of the following:
- the transaction was included despite having invalid preconditions
- the transaction nonce was not equal to the account's nonce
- the transaction tried to remove a signer not in the account
- the transaction tried to add a signer already in the account
- the transaction tried to add a signer when the signers array was full
- the output state root does not match the expected result of applying the state transition
Input
previousRoot
- root hash of the state tree prior to the transactionstateProof
- encodedStateProof
transaction
-SoftChangeSigner
struct
Process
- Get the return values
(accountIndex, siblings, account)
fromverifyAccountInState(previousRoot, stateProof)
- Verify that
accountIndex == transaction.accountIndex
, revert the transaction otherwise - If
transaction.nonce != account.nonce
, return - If
transaction.modificationCategory == 0
it is an add signer transaction:- If the account already had the signer address, return.
- If the account's signers array was full (10 members), return.
- Add
transaction.signerAddress
toaccount.signers
- Otherwise it was a remove signer transaction:
- If the account did not have the signer address, return.
- Remove the signer address from
account.signers
- Set
account.nonce += 1
- Recalculate the state root with
updateAccount(account, accountIndex, siblings)
- If the new root is equal to the transaction's intermediate state root, revert the transaction.