Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion out/Escrow.sol/Escrow.json

Large diffs are not rendered by default.

39 changes: 25 additions & 14 deletions src/Escrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ contract Escrow {
// Based on Nomad's proof structure
struct ReceiptProof {
bytes blockHeader; // RLP-encoded block header
bytes receiptRlp; // RLP-encoded target receipt
bytes proofNodes; // RLP-encoded array of MPT proof nodes
bytes receiptPath; // RLP-encoded receipt index
uint256 logIndex; // Index of target log in receipt
bytes proofNodes; // RLP-encoded array of shared MPT proof nodes
bytes[] receiptPaths; // RLP-encoded receipt indices proven by the multi-proof
bytes[] receiptRlps; // RLP-encoded receipts corresponding to the paths
uint256[] logIndices; // Target log index for each receipt
}

constructor(
Expand Down Expand Up @@ -112,7 +112,7 @@ contract Escrow {
cancellationRequest = false;
}

// Now validates a given merkle proof against a recent block hash and checks the Transfer event's contents
// Now validates a shared receipt MPT multiproof against a recent block hash and checks the Transfer event's contents
function collect(ReceiptProof calldata proof, uint256 targetBlockNumber) public {
require(funded, "Contract not funded");
require(msg.sender == bondedExecutor && is_bonded(), "Only bonded executor can collect");
Expand All @@ -136,20 +136,31 @@ contract Escrow {
// Extract receipts root from block header
bytes32 receiptsRoot = BlockHeaderParser.extractReceiptsRoot(proof.blockHeader);

// Verify receipt proof against receipts root using MPT verification
require(
MPTVerifier.verifyReceiptProof(proof.receiptRlp, proof.proofNodes, proof.receiptPath, receiptsRoot),
"Invalid receipt MPT proof"
);
uint256 receiptCount = proof.receiptRlps.length;
require(receiptCount > 0, "No receipts provided");
require(receiptCount == proof.receiptPaths.length, "Receipt/path length mismatch");
require(receiptCount == proof.logIndices.length, "Receipt/log index length mismatch");

// Extract and validate the Transfer event
// Verify receipt multiproof against receipts root using MPT verification
require(
ReceiptValidator.validateTransferInReceipt(
proof.receiptRlp, proof.logIndex, tokenContract, expectedRecipient, expectedAmount
),
MPTVerifier.verifyReceiptMultiProof(proof.receiptRlps, proof.receiptPaths, proof.proofNodes, receiptsRoot),
"Invalid Transfer event"
);

// Extract and validate the expected Transfer event in one of the proven receipts
bool transferValidated = false;
for (uint256 i = 0; i < receiptCount; i++) {
if (
ReceiptValidator.validateTransferInReceipt(
proof.receiptRlps[i], proof.logIndices[i], tokenContract, expectedRecipient, expectedAmount
)
) {
transferValidated = true;
break;
}
}
require(transferValidated, "Expected Transfer event not found");

uint256 payout = bondAmount + currentRewardAmount + currentPaymentAmount;
address executor = bondedExecutor;

Expand Down
177 changes: 165 additions & 12 deletions src/MPTVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,51 @@ library MPTVerifier {
using RLPParser for bytes;

/**
* @dev Verify receipt inclusion using Merkle Patricia Trie proof
* @param receiptRlp RLP-encoded transaction receipt
* @param proofNodes RLP-encoded array of MPT proof nodes
* @param receiptPath RLP-encoded transaction index (key)
* @dev Verify one or more receipt inclusions using a shared Merkle Patricia Trie proof.
* @param receiptRlps RLP-encoded receipts being proven
* @param receiptPaths RLP-encoded transaction indices (keys) for each receipt
* @param proofNodes RLP-encoded array of shared MPT proof nodes
* @param receiptsRoot Root hash of the receipts trie
* @return True if the proof is valid
* @return True if every (path, receipt) pair is proven
*/
function verifyReceiptMultiProof(
bytes[] calldata receiptRlps,
bytes[] calldata receiptPaths,
bytes calldata proofNodes,
bytes32 receiptsRoot
) internal pure returns (bool) {
bytes[] memory receiptsMem = receiptRlps;
bytes[] memory pathsMem = receiptPaths;
bytes memory proofNodesMem = proofNodes;
return verifyReceiptMultiProofInternal(receiptsMem, pathsMem, proofNodesMem, receiptsRoot);
}

/**
* @dev Memory-based entry point used by tests or callers already working in memory.
*/
function verifyReceiptMultiProofFromMemory(
bytes[] memory receiptRlps,
bytes[] memory receiptPaths,
bytes memory proofNodes,
bytes32 receiptsRoot
) internal pure returns (bool) {
return verifyReceiptMultiProofInternal(receiptRlps, receiptPaths, proofNodes, receiptsRoot);
}

function verifyReceiptProof(
bytes calldata receiptRlp,
bytes calldata proofNodes,
bytes calldata receiptPath,
bytes32 receiptsRoot
) internal pure returns (bool) {
// Key is the RLP-encoded tx index BYTES (unmodified)
bytes memory key = receiptPath;
// Value is EXACT receipt bytes
bytes memory value = receiptRlp;

// Verify MPT proof
return verifyProof(key, value, proofNodes, receiptsRoot);
bytes[] memory receipts = new bytes[](1);
bytes[] memory paths = new bytes[](1);
bytes memory singleReceipt = receiptRlp;
bytes memory singlePath = receiptPath;
receipts[0] = singleReceipt;
paths[0] = singlePath;
bytes memory proofNodesMem = proofNodes;
return verifyReceiptMultiProofInternal(receipts, paths, proofNodesMem, receiptsRoot);
}

/**
Expand Down Expand Up @@ -117,6 +142,134 @@ library MPTVerifier {
return false;
}

function decodeProofNodes(bytes memory proofNodes) private pure returns (bytes[] memory nodes) {
require(proofNodes.length > 0 && proofNodes[0] >= 0xc0, "Invalid proof node list");

uint256 offset;
if (uint8(proofNodes[0]) >= 0xf8) {
offset = 1 + (uint8(proofNodes[0]) - 0xf7);
} else {
offset = 1;
}

uint256 count = countListItems(proofNodes, offset);
nodes = new bytes[](count);

uint256 currentOffset = offset;
for (uint256 i = 0; i < count; i++) {
(bytes memory node, uint256 nodeLength) = proofNodes.parseItem(currentOffset);
nodes[i] = node;
currentOffset += nodeLength;
}
}

function verifyReceiptMultiProofInternal(
bytes[] memory receiptRlps,
bytes[] memory receiptPaths,
bytes memory proofNodes,
bytes32 receiptsRoot
) private pure returns (bool) {
require(receiptRlps.length == receiptPaths.length, "Mismatched receipt inputs");

bytes[] memory nodes = decodeProofNodes(proofNodes);

for (uint256 i = 0; i < receiptRlps.length; i++) {
if (!verifySingleWithNodes(receiptPaths[i], receiptRlps[i], nodes, receiptsRoot)) {
return false;
}
}
return true;
}

function verifySingleWithNodes(bytes memory key, bytes memory value, bytes[] memory nodes, bytes32 root)
private
pure
returns (bool)
{
bytes32 currentHash = root;
uint256 keyOffset = 0;

while (true) {
(bool found, bytes memory node) = fetchNode(nodes, currentHash);
if (!found) {
return false;
}

if (node.length == 0 || node[0] < 0xc0) {
return false;
}

uint256 nodeOffset = 1;
if (node[0] >= 0xf8) {
nodeOffset += uint8(node[0]) - 0xf7;
}

uint256 items = countListItems(node, nodeOffset);

if (items == 17) {
(bool success, uint256 newKeyOffset, bytes32 newHash) =
processBranchNode(node, nodeOffset, key, keyOffset, value);
if (!success) {
return false;
}
if (newHash == bytes32(0)) {
return true;
}
keyOffset = newKeyOffset;
currentHash = newHash;
} else if (items == 2) {
(bool success, uint256 newKeyOffset, bytes32 newHash) =
processLeafOrExtensionNode(node, nodeOffset, key, keyOffset, value);
if (!success) {
return false;
}
if (newHash == bytes32(0)) {
return true;
}
keyOffset = newKeyOffset;
currentHash = newHash;
} else {
return false;
}
}

return false;
}

function fetchNode(bytes[] memory nodes, bytes32 hash) private pure returns (bool, bytes memory) {
for (uint256 i = 0; i < nodes.length; i++) {
bytes memory candidate = nodes[i];
if (keccak256(candidate) == hash) {
return (true, candidate);
}
if (candidate.length == 32) {
bytes32 direct;
assembly {
direct := mload(add(candidate, 32))
}
if (direct == hash) {
return (true, candidate);
}
}
if (candidate.length == 0) {
continue;
}
if (candidate.length < 32) {
bytes32 shortHash = keccak256(candidate);
if (shortHash == hash) {
return (true, candidate);
}
}
if (candidate.length > 32) {
bytes32 shortHash2 = keccak256(candidate);
if (shortHash2 == hash) {
return (true, candidate);
}
}
}
return (false, bytes(""));
}

/**
* @dev Count RLP items in a list
* @param data The RLP encoded list
Expand Down
Loading