Skip to content

Commit

Permalink
feat: change impl to process transaction receipts (#99)
Browse files Browse the repository at this point in the history
Signed-off-by: Jawad Tariq <[email protected]>
Co-authored-by: Sébastien Dan <[email protected]>
  • Loading branch information
JDawg287 and sebastiendan committed Aug 28, 2023
1 parent 9320618 commit 597bf3a
Show file tree
Hide file tree
Showing 15 changed files with 389 additions and 302 deletions.
37 changes: 26 additions & 11 deletions contracts/examples/ERC20Messaging.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,13 @@ contract ERC20Messaging is IERC20Messaging, ToposMessaging {
/// @notice Entry point for sending a cross-subnet asset transfer
/// @dev The input data is sent to the target subnet externally
/// @param targetSubnetId Target subnet ID
/// @param /*receiver*/ Receiver's address (avoiding unused local variable warning)
/// @param tokenAddress Address of target token contract
/// @param receiver Receiver's address
/// @param amount Amount of token to send
function sendToken(SubnetId targetSubnetId, address /*receiver*/, address tokenAddress, uint256 amount) external {
function sendToken(SubnetId targetSubnetId, address tokenAddress, address receiver, uint256 amount) external {
if (_toposCoreAddr.code.length == uint256(0)) revert InvalidToposCore();
_burnTokenFrom(msg.sender, tokenAddress, amount);
emit TokenSent(targetSubnetId, tokenAddress, receiver, amount);
_emitMessageSentEvent(targetSubnetId);
}

Expand Down Expand Up @@ -136,16 +137,30 @@ contract ERC20Messaging is IERC20Messaging, ToposMessaging {
}

/// @notice Execute a cross-subnet asset transfer
/// @param indexOfDataInTxRaw Index of data in txRaw
/// @param txRaw Raw transaction data
function _execute(uint256 indexOfDataInTxRaw, bytes calldata txRaw) internal override {
(SubnetId targetSubnetId, address receiver, address tokenAddress, uint256 amount) = abi.decode(
txRaw[indexOfDataInTxRaw + 4:], // omit the 4 bytes function selector
(SubnetId, address, address, uint256)
/// @param logIndexes Array of indexes of the logs to use
/// @param logsAddress Array of addresses of the logs
/// @param logsData Array of data of the logs
/// @param logsTopics Array of topics of the logs
function _execute(
uint256[] memory logIndexes,
address[] memory logsAddress,
bytes[] memory logsData,
bytes32[][] memory logsTopics,
SubnetId networkSubnetId
) internal override {
// verify that the event was emitted by this contract on the source subnet
uint256 tokenSentEventIndex = logIndexes[0];
if (logsAddress[tokenSentEventIndex] != address(this)) revert InvalidOriginAddress();

// implication on the application contract to verify the target subnet id
// first topic is the event signature & second topic is the target subnet id
bytes32 targetSubnetId = logsTopics[tokenSentEventIndex][1];
if (SubnetId.unwrap(networkSubnetId) != targetSubnetId) revert InvalidSubnetId();

(address tokenAddress, address receiver, uint256 amount) = abi.decode(
logsData[tokenSentEventIndex],
(address, address, uint256)
);
if (!_validateTargetSubnetId(targetSubnetId)) revert InvalidSubnetId();

// prevent reentrancy
_mintToken(tokenAddress, receiver, amount);
}

Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/IERC20Messaging.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ interface IERC20Messaging is IToposMessaging {

event TokenDeployed(string symbol, address tokenAddress);

event TokenSent(SubnetId indexed targetSubnetId, address tokenAddress, address receiver, uint256 amount);

error BurnFailed(address tokenAddress);
error ExceedDailyMintLimit(address tokenAddress);
error InvalidAmount();
error InvalidOriginAddress();
error InvalidSetDailyMintLimitsParams();
error InvalidSubnetId();
error InvalidTokenDeployer();
error TokenAlreadyExists(address tokenAddress);
error TokenDeployFailed();
Expand Down
7 changes: 4 additions & 3 deletions contracts/interfaces/IToposCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface IToposCore {
SubnetId sourceSubnetId;
bytes32 stateRoot;
bytes32 txRoot;
bytes32 receiptRoot;
SubnetId[] targetSubnets;
uint32 verifier;
CertificateId certId;
Expand All @@ -22,9 +23,9 @@ interface IToposCore {
SubnetId sourceSubnetId;
}

event CertStored(CertificateId certId, bytes32 txRoot);
event CertStored(CertificateId certId, bytes32 receiptRoot);

event CrossSubnetMessageSent(SubnetId targetSubnetId);
event CrossSubnetMessageSent(SubnetId indexed targetSubnetId);

event Upgraded(address indexed implementation);

Expand Down Expand Up @@ -84,5 +85,5 @@ interface IToposCore {

function sourceSubnetIdExists(SubnetId subnetId) external view returns (bool);

function txRootToCertId(bytes32 txRoot) external view returns (CertificateId);
function receiptRootToCertId(bytes32 receiptRoot) external view returns (CertificateId);
}
11 changes: 8 additions & 3 deletions contracts/interfaces/IToposMessaging.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ interface IToposMessaging {
}

error CertNotPresent();
error IllegalMemoryAccess();
error InvalidMerkleProof();
error InvalidSubnetId();
error InvalidTransactionStatus();
error InvalidToposCore();
error LogIndexOutOfRange();
error TransactionAlreadyExecuted();
error UnsupportedProofKind();

function validateMerkleProof(bytes memory proofBlob, bytes32 txHash, bytes32 txRoot) external returns (bool);
function execute(uint256[] calldata logIndexes, bytes calldata proofBlob, bytes32 receiptRoot) external;

function validateMerkleProof(
bytes memory proofBlob,
bytes32 receiptRoot
) external returns (bytes memory receiptRaw);

function toposCore() external view returns (address);
}
14 changes: 8 additions & 6 deletions contracts/topos-core/ToposCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable {
/// @notice Mapping to store the last seen certificate for a subnet
mapping(SubnetId => IToposCore.StreamPosition) checkpoint;

/// @notice Mapping of transactions root to the certificate ID
mapping(bytes32 => CertificateId) public txRootToCertId;
/// @notice Mapping of receipts root to the certificate ID
mapping(bytes32 => CertificateId) public receiptRootToCertId;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -63,14 +63,15 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable {
SubnetId sourceSubnetId,
bytes32 stateRoot,
bytes32 txRoot,
bytes32 receiptRoot,
SubnetId[] memory targetSubnets,
uint32 verifier,
CertificateId certId,
bytes memory starkProof,
bytes memory signature
) = abi.decode(
certBytes,
(CertificateId, SubnetId, bytes32, bytes32, SubnetId[], uint32, CertificateId, bytes, bytes)
(CertificateId, SubnetId, bytes32, bytes32, bytes32, SubnetId[], uint32, CertificateId, bytes, bytes)
);

certificateSet.insert(CertificateId.unwrap(certId)); // add certificate ID to the CRUD storage set
Expand All @@ -79,6 +80,7 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable {
newCert.sourceSubnetId = sourceSubnetId;
newCert.stateRoot = stateRoot;
newCert.txRoot = txRoot;
newCert.receiptRoot = receiptRoot;
newCert.targetSubnets = targetSubnets;
newCert.verifier = verifier;
newCert.certId = certId;
Expand All @@ -93,8 +95,8 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable {
newStreamPosition.position = position;
newStreamPosition.sourceSubnetId = sourceSubnetId;

txRootToCertId[txRoot] = certId; // add certificate ID to the transaction root mapping
emit CertStored(certId, txRoot);
receiptRootToCertId[receiptRoot] = certId; // add certificate ID to the receipt root mapping
emit CertStored(certId, receiptRoot);
}

/// @notice Emits an event to signal a cross subnet message has been sent
Expand Down Expand Up @@ -198,7 +200,7 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable {
storedCert.prevId,
storedCert.sourceSubnetId,
storedCert.stateRoot,
storedCert.txRoot,
storedCert.receiptRoot,
storedCert.targetSubnets,
storedCert.verifier,
storedCert.certId,
Expand Down
147 changes: 101 additions & 46 deletions contracts/topos-core/ToposMessaging.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,83 +26,96 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
}

/// @notice Entry point for executing any message on a target subnet
/// @param indexOfDataInTxRaw Index of tx.data in raw transaction hex
/// @param txRaw RLP encoded raw transaction hex
/// @param logIndexes Indexes of the logs to process
/// @param proofBlob RLP encoded proof blob
/// @param txRoot Transactions root
function execute(
uint256 indexOfDataInTxRaw,
bytes calldata proofBlob,
bytes calldata txRaw,
bytes32 txRoot
) external {
/// @param receiptRoot Receipts root of the block
function execute(uint256[] calldata logIndexes, bytes calldata proofBlob, bytes32 receiptRoot) external {
if (_toposCoreAddr.code.length == uint256(0)) revert InvalidToposCore();
if (txRaw.length < indexOfDataInTxRaw + 4) revert IllegalMemoryAccess();
if (logIndexes.length < 1) revert LogIndexOutOfRange();

CertificateId certId = IToposCore(_toposCoreAddr).txRootToCertId(txRoot);
CertificateId certId = IToposCore(_toposCoreAddr).receiptRootToCertId(receiptRoot);
if (!IToposCore(_toposCoreAddr).certificateExists(certId)) revert CertNotPresent();

// In order to validate the transaction pass the entire transaction bytes which is then hashed.
// The transaction hash is used as a leaf to validate the inclusion proof.
bytes32 txHash = keccak256(abi.encodePacked(txRaw));
if (!validateMerkleProof(proofBlob, txHash, txRoot)) revert InvalidMerkleProof();
// the raw receipt bytes are taken out of the proof
bytes memory receiptRaw = validateMerkleProof(proofBlob, receiptRoot);
if (receiptRaw.length == uint256(0)) revert InvalidMerkleProof();

bytes32 receiptHash = keccak256(abi.encodePacked(receiptRaw));
if (_isTxExecuted(receiptHash, receiptRoot)) revert TransactionAlreadyExecuted();

(
uint256 status, // uint256 cumulativeGasUsed // bytes memory logsBloom
,
,
address[] memory logsAddress,
bytes32[][] memory logsTopics,
bytes[] memory logsData
) = _decodeReceipt(receiptRaw);
if (status != 1) revert InvalidTransactionStatus();

// verify that provided indexes are within the range of the number of event logs
for (uint256 i = 0; i < logIndexes.length; i++) {
if (logIndexes[i] >= logsAddress.length) revert LogIndexOutOfRange();
}

if (_isTxExecuted(txHash)) revert TransactionAlreadyExecuted();
SubnetId networkSubnetId = IToposCore(_toposCoreAddr).networkSubnetId();

// prevent re-entrancy
_setTxExecuted(txHash);
_execute(indexOfDataInTxRaw, txRaw);
_setTxExecuted(receiptHash, receiptRoot);
_execute(logIndexes, logsAddress, logsData, logsTopics, networkSubnetId);
}

/// @notice Get the address of topos core contract
function toposCore() public view returns (address) {
return _toposCoreAddr;
}

/// @notice Validate a Merkle proof for an external transaction
/// @notice Validate a Merkle proof for an external transaction receipt
/// @param proofBlob RLP encoded proof blob
/// @param txHash Transaction hash
/// @param txRoot Transactions root
/// @param receiptRoot Receipts root of the block
function validateMerkleProof(
bytes memory proofBlob,
bytes32 txHash,
bytes32 txRoot
) public pure override returns (bool) {
bytes32 receiptRoot
) public pure override returns (bytes memory receiptRaw) {
Proof memory proof = _decodeProofBlob(proofBlob);

if (proof.kind != 1) revert UnsupportedProofKind();

bytes memory txRawFromProof = MerklePatriciaProofVerifier.extractProofValue(txRoot, proof.mptKey, proof.stack);
if (txRawFromProof.length == 0) {
// Empty return value for proof of exclusion
return false;
} else {
bytes32 txHashFromProof = keccak256(abi.encodePacked(txRawFromProof));
return txHash == txHashFromProof;
}
receiptRaw = MerklePatriciaProofVerifier.extractProofValue(receiptRoot, proof.mptKey, proof.stack);
}

/// @notice Execute the message on a target subnet
/// @dev This function should be implemented by the child contract
/// @param indexOfDataInTxRaw Index of tx.data in raw transaction hex
/// @param txRaw RLP encoded raw transaction hex
function _execute(uint256 indexOfDataInTxRaw, bytes calldata txRaw) internal virtual {}
/// @param logIndexes Array of indexes of the logs to use
/// @param logsAddress Array of addresses of the logs
/// @param logsData Array of data of the logs
/// @param logsTopics Array of topics of the logs
/// @param networkSubnetId Subnet id of the network
function _execute(
uint256[] memory logIndexes,
address[] memory logsAddress,
bytes[] memory logsData,
bytes32[][] memory logsTopics,
SubnetId networkSubnetId
) internal virtual {}

/// @notice emit a message sent event from the ToposCore contract
function _emitMessageSentEvent(SubnetId targetSubnetId) internal {
IToposCore(_toposCoreAddr).emitCrossSubnetMessage(targetSubnetId);
}

/// @notice Set a flag to indicate that the asset transfer transaction has been executed
/// @param txHash Hash of asset transfer transaction
function _setTxExecuted(bytes32 txHash) internal {
_setBool(_getTxExecutedKey(txHash), true);
/// @param receiptHash receipt hash
/// @param receiptRoot receipt root
function _setTxExecuted(bytes32 receiptHash, bytes32 receiptRoot) internal {
bytes32 suffix = keccak256(abi.encodePacked(receiptHash, receiptRoot));
_setBool(_getTxExecutedKey(suffix), true);
}

/// @notice Get the flag to indicate that the transaction has been executed
/// @param txHash transaction hash
function _isTxExecuted(bytes32 txHash) internal view returns (bool) {
return getBool(_getTxExecutedKey(txHash));
/// @param receiptHash receipt hash
/// @param receiptRoot receipt root
function _isTxExecuted(bytes32 receiptHash, bytes32 receiptRoot) internal view returns (bool) {
bytes32 suffix = keccak256(abi.encodePacked(receiptHash, receiptRoot));
return getBool(_getTxExecutedKey(suffix));
}

/// @notice Validate that the target subnet id is the same as the subnet id of the topos core
Expand All @@ -113,9 +126,9 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
}

/// @notice Get the key for the flag to indicate that the transaction has been executed
/// @param txHash transaction hash
function _getTxExecutedKey(bytes32 txHash) internal pure returns (bytes32) {
return keccak256(abi.encode(PREFIX_EXECUTED, txHash));
/// @param suffix suffix of the key
function _getTxExecutedKey(bytes32 suffix) internal pure returns (bytes32) {
return keccak256(abi.encode(PREFIX_EXECUTED, suffix));
}

/// @notice Decode the proof blob into Proof struct
Expand Down Expand Up @@ -156,4 +169,46 @@ contract ToposMessaging is IToposMessaging, EternalStorage {

assert(nibblesLength == nibbles.length);
}

/// @notice Decode the receipt into its components
/// @param receiptRaw RLP encoded receipt
function _decodeReceipt(
bytes memory receiptRaw
)
internal
pure
returns (
uint256 status,
uint256 cumulativeGasUsed,
bytes memory logsBloom,
address[] memory logsAddress,
bytes32[][] memory logsTopics,
bytes[] memory logsData
)
{
RLPReader.RLPItem[] memory receipt = receiptRaw.toRlpItem().toList();

status = receipt[0].toUint();
cumulativeGasUsed = receipt[1].toUint();
logsBloom = receipt[2].toBytes();

RLPReader.RLPItem[] memory logs = receipt[3].toList();
logsAddress = new address[](logs.length);
logsTopics = new bytes32[][](logs.length);
logsData = new bytes[](logs.length);

for (uint256 i = 0; i < logs.length; i++) {
RLPReader.RLPItem[] memory log = logs[i].toList();
logsAddress[i] = log[0].toAddress();

RLPReader.RLPItem[] memory topics = log[1].toList();
bytes32[] memory topicArray = new bytes32[](topics.length);
for (uint256 j = 0; j < topics.length; j++) {
topicArray[j] = bytes32(topics[j].toUint());
}
logsTopics[i] = topicArray;

logsData[i] = log[2].toBytes();
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@topos-protocol/topos-smart-contracts",
"version": "1.1.4",
"version": "1.1.5-rc1",
"description": "Topos Smart Contracts",
"repository": {
"type": "git",
Expand Down
Loading

0 comments on commit 597bf3a

Please sign in to comment.