Skip to content

Commit

Permalink
feat: add unique receipts for cross-subnet transactions (#125)
Browse files Browse the repository at this point in the history
Signed-off-by: Jawad Tariq <[email protected]>
  • Loading branch information
JDawg287 authored Feb 22, 2024
1 parent d64e219 commit 4b819f4
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 39 deletions.
2 changes: 1 addition & 1 deletion contracts/interfaces/IToposCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface IToposCore {

event CertStored(CertificateId certId, bytes32 receiptRoot);

event CrossSubnetMessageSent(SubnetId indexed targetSubnetId);
event CrossSubnetMessageSent(SubnetId indexed targetSubnetId, SubnetId sourceSubnetId, uint256 nonce);

event Upgraded(address indexed implementation);

Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/IToposMessaging.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface IToposMessaging {
function validateMerkleProof(
bytes memory proofBlob,
bytes32 receiptRoot
) external returns (bytes memory receiptRaw);
) external returns (bytes memory receiptTrieNodeRaw);

function toposCore() external view returns (address);
}
4 changes: 2 additions & 2 deletions contracts/topos-core/CodeHash.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ pragma solidity ^0.8.9;
contract CodeHash {
/// @notice gets the codehash of a contract address
/// @param contractAddr a contract address
function getCodeHash(address contractAddr) public view returns (bytes32) {
function getCodeHash(address contractAddr) public view returns (bytes32 codeHash) {
// does not fail with wallet addresses
if (contractAddr.codehash.length != 0) {
return contractAddr.codehash;
codeHash = contractAddr.codehash;
}
}
}
7 changes: 6 additions & 1 deletion contracts/topos-core/ToposCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable {
/// @notice The subnet ID of the subnet this contract is to be deployed on
SubnetId public networkSubnetId;

/// @notice Nonce for cross subnet message, meant to be used in combination with `sourceSubnetId`
/// so that the message can be uniquely identified per subnet
uint256 private nonce;

/// @notice Set of certificate IDs
Bytes32SetsLib.Set certificateSet;

Expand Down Expand Up @@ -113,7 +117,8 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable {
/// @notice Emits an event to signal a cross subnet message has been sent
/// @param targetSubnetId The subnet ID of the target subnet
function emitCrossSubnetMessage(SubnetId targetSubnetId) external {
emit CrossSubnetMessageSent(targetSubnetId);
nonce += 1;
emit CrossSubnetMessageSent(targetSubnetId, networkSubnetId, nonce);
}

/// @notice Returns the admin epoch
Expand Down
34 changes: 17 additions & 17 deletions contracts/topos-core/ToposMessaging.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
if (!IToposCore(_toposCoreAddr).certificateExists(certId)) revert CertNotPresent();

// the raw receipt bytes are taken out of the proof
bytes memory receiptRaw = validateMerkleProof(proofBlob, receiptRoot);
if (receiptRaw.length == uint256(0)) revert InvalidMerkleProof();
bytes memory receiptTrieNodeRaw = validateMerkleProof(proofBlob, receiptRoot);
if (receiptTrieNodeRaw.length == uint256(0)) revert InvalidMerkleProof();

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

(
uint256 status, // uint256 cumulativeGasUsed // bytes memory logsBloom
Expand All @@ -50,7 +50,7 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
address[] memory logsAddress,
bytes32[][] memory logsTopics,
bytes[] memory logsData
) = _decodeReceipt(receiptRaw);
) = _decodeReceipt(receiptTrieNodeRaw);
if (status != 1) revert InvalidTransactionStatus();

// verify that provided indexes are within the range of the number of event logs
Expand All @@ -61,7 +61,7 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
SubnetId networkSubnetId = IToposCore(_toposCoreAddr).networkSubnetId();

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

Expand All @@ -76,10 +76,10 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
function validateMerkleProof(
bytes memory proofBlob,
bytes32 receiptRoot
) public pure override returns (bytes memory receiptRaw) {
) public pure override returns (bytes memory receiptTrieNodeRaw) {
Proof memory proof = _decodeProofBlob(proofBlob);
if (proof.kind != 1) revert UnsupportedProofKind();
receiptRaw = MerklePatriciaProofVerifier.extractProofValue(receiptRoot, proof.mptKey, proof.stack);
receiptTrieNodeRaw = MerklePatriciaProofVerifier.extractProofValue(receiptRoot, proof.mptKey, proof.stack);
}

/// @notice Execute the message on a target subnet
Expand All @@ -103,18 +103,18 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
}

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

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

Expand Down Expand Up @@ -171,9 +171,9 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
}

/// @notice Decode the receipt into its components
/// @param receiptRaw RLP encoded receipt
/// @param receiptTrieNodeRaw RLP encoded receipt
function _decodeReceipt(
bytes memory receiptRaw
bytes memory receiptTrieNodeRaw
)
internal
pure
Expand All @@ -186,7 +186,7 @@ contract ToposMessaging is IToposMessaging, EternalStorage {
bytes[] memory logsData
)
{
RLPReader.RLPItem[] memory receipt = receiptRaw.toRlpItem().toList();
RLPReader.RLPItem[] memory receipt = receiptTrieNodeRaw.toRlpItem().toList();

status = receipt[0].toUint();
cumulativeGasUsed = receipt[1].toUint();
Expand Down
110 changes: 93 additions & 17 deletions test/topos-core/ToposMessaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@ describe('ToposMessaging', () => {
await toposCoreProxy.waitForDeployment()
const toposCoreProxyAddress = await toposCoreProxy.getAddress()

const toposCore = await ToposCore__factory.connect(
toposCoreProxyAddress,
admin
)
const toposCore = ToposCore__factory.connect(toposCoreProxyAddress, admin)
await toposCore.initialize(adminAddresses, adminThreshold)

const erc20Messaging = await new ERC20Messaging__factory(admin).deploy(
Expand Down Expand Up @@ -214,7 +211,7 @@ describe('ToposMessaging', () => {
(l) => (l as EventLog).eventName === 'TokenDeployed'
) as EventLog
const tokenAddress = log.args.tokenAddress
const erc20 = await ERC20__factory.connect(tokenAddress, admin)
const erc20 = ERC20__factory.connect(tokenAddress, admin)
await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50)

const sendToken = await sendTokenTx(
Expand All @@ -223,7 +220,8 @@ describe('ToposMessaging', () => {
receiver.address,
admin,
cc.SOURCE_SUBNET_ID_2,
tc.TOKEN_SYMBOL_X
tc.TOKEN_SYMBOL_X,
tc.SEND_AMOUNT_50
)

const { proofBlob, receiptsRoot } = await getReceiptMptProof(
Expand Down Expand Up @@ -498,7 +496,7 @@ describe('ToposMessaging', () => {
const deployedToken = await erc20Messaging.getTokenBySymbol(
tc.TOKEN_SYMBOL_X
)
const erc20 = await ERC20__factory.connect(deployedToken.addr, admin)
const erc20 = ERC20__factory.connect(deployedToken.addr, admin)
await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50)

const sendToken = await sendTokenTx(
Expand All @@ -507,7 +505,8 @@ describe('ToposMessaging', () => {
receiver.address,
admin,
cc.SOURCE_SUBNET_ID_2,
tc.TOKEN_SYMBOL_X
tc.TOKEN_SYMBOL_X,
tc.SEND_AMOUNT_50
)

const { proofBlob, receiptsRoot } = await getReceiptMptProof(
Expand Down Expand Up @@ -547,7 +546,7 @@ describe('ToposMessaging', () => {
const deployedToken = await erc20Messaging.getTokenBySymbol(
tc.TOKEN_SYMBOL_X
)
const erc20 = await ERC20__factory.connect(deployedToken.addr, admin)
const erc20 = ERC20__factory.connect(deployedToken.addr, admin)
await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50)

const sendToken = await sendTokenTx(
Expand All @@ -556,7 +555,8 @@ describe('ToposMessaging', () => {
ethers.ZeroAddress, // sending to a zero address
admin,
cc.SOURCE_SUBNET_ID_2,
tc.TOKEN_SYMBOL_X
tc.TOKEN_SYMBOL_X,
tc.SEND_AMOUNT_50
)

const { proofBlob, receiptsRoot } = await getReceiptMptProof(
Expand All @@ -582,6 +582,80 @@ describe('ToposMessaging', () => {
).to.be.revertedWith('ERC20: mint to the zero address')
})

it('can execute a transaction with same inputs twice', async () => {
const { admin, receiver, defaultToken, toposCore, erc20Messaging } =
await loadFixture(deployERC20MessagingFixture)
await toposCore.setNetworkSubnetId(cc.SOURCE_SUBNET_ID_2)
// first transaction
const { erc20, proofBlob, receiptsRoot } = await deployDefaultToken(
admin,
receiver,
defaultToken,
erc20Messaging
)
const certificate = testUtils.encodeCertParam(
cc.PREV_CERT_ID_0,
cc.SOURCE_SUBNET_ID_1,
cc.STATE_ROOT_MAX,
cc.TX_ROOT_MAX,
receiptsRoot,
[cc.SOURCE_SUBNET_ID_2],
cc.VERIFIER,
cc.CERT_ID_1,
cc.DUMMY_STARK_PROOF,
cc.DUMMY_SIGNATURE
)
await toposCore.pushCertificate(certificate, cc.CERT_POS_1)
await expect(
erc20Messaging.execute([tc.TOKEN_SENT_INDEX_2], proofBlob, receiptsRoot)
)
.to.emit(erc20, 'Transfer')
.withArgs(ethers.ZeroAddress, receiver.address, tc.SEND_AMOUNT_50)
await expect(erc20.balanceOf(receiver.address)).to.eventually.equal(
tc.SEND_AMOUNT_50
)

// second transaction
await erc20.approve(await erc20Messaging.getAddress(), tc.SEND_AMOUNT_50)
const sendToken = await sendTokenTx(
erc20Messaging,
ethers.provider,
await receiver.getAddress(),
admin,
cc.SOURCE_SUBNET_ID_2,
await erc20.symbol(),
tc.SEND_AMOUNT_50
)

const { proofBlob: proofBlob2, receiptsRoot: receiptsRoot2 } =
await getReceiptMptProof(sendToken, ethers.provider)
const certificate2 = testUtils.encodeCertParam(
cc.CERT_ID_1,
cc.SOURCE_SUBNET_ID_1,
cc.STATE_ROOT_MAX,
cc.TX_ROOT_MAX,
receiptsRoot2,
[cc.SOURCE_SUBNET_ID_2],
cc.VERIFIER,
cc.CERT_ID_2,
cc.DUMMY_STARK_PROOF,
cc.DUMMY_SIGNATURE
)
await toposCore.pushCertificate(certificate2, cc.CERT_POS_2)
await expect(
erc20Messaging.execute(
[tc.TOKEN_SENT_INDEX_2],
proofBlob2,
receiptsRoot2
)
)
.to.emit(erc20, 'Transfer')
.withArgs(ethers.ZeroAddress, receiver.address, tc.SEND_AMOUNT_50)
await expect(erc20.balanceOf(receiver.address)).to.eventually.equal(
tc.SEND_AMOUNT_50 * 2
)
})

it('emits the Transfer success event', async () => {
const { admin, receiver, defaultToken, toposCore, erc20Messaging } =
await loadFixture(deployERC20MessagingFixture)
Expand Down Expand Up @@ -680,7 +754,7 @@ describe('ToposMessaging', () => {
(l) => (l as EventLog).eventName === 'TokenDeployed'
) as EventLog
const tokenAddress = log.args.tokenAddress
const erc20 = await ERC20__factory.connect(tokenAddress, admin)
const erc20 = ERC20__factory.connect(tokenAddress, admin)
await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50)

await expect(
Expand All @@ -702,7 +776,7 @@ describe('ToposMessaging', () => {
tc.SEND_AMOUNT_50
)
.to.emit(toposCore, 'CrossSubnetMessageSent')
.withArgs(cc.TARGET_SUBNET_ID_4)
.withArgs(cc.TARGET_SUBNET_ID_4, cc.SOURCE_SUBNET_ID_2, 1)
})
})

Expand All @@ -719,7 +793,7 @@ describe('ToposMessaging', () => {
(l) => (l as EventLog).eventName === 'TokenDeployed'
) as EventLog
const tokenAddress = log.args.tokenAddress
const erc20 = await ERC20__factory.connect(tokenAddress, admin)
const erc20 = ERC20__factory.connect(tokenAddress, admin)
await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50)

const receiverAddress = await receiver.getAddress()
Expand All @@ -730,7 +804,8 @@ describe('ToposMessaging', () => {
receiverAddress,
admin,
cc.SOURCE_SUBNET_ID_2,
await erc20.symbol()
await erc20.symbol(),
tc.SEND_AMOUNT_50
)

const { proofBlob, receiptsRoot } = await getReceiptMptProof(
Expand All @@ -746,20 +821,21 @@ describe('ToposMessaging', () => {
receiver: string,
signer: Signer,
targetSubnetId: string,
symbol: string
symbol: string,
amount: number
) {
const estimatedGasLimit = await erc20Messaging.sendToken.estimateGas(
targetSubnetId,
symbol,
receiver,
tc.SEND_AMOUNT_50,
amount,
{ gasLimit: 4_000_000 }
)
const TxUnsigned = await erc20Messaging.sendToken.populateTransaction(
targetSubnetId,
symbol,
receiver,
tc.SEND_AMOUNT_50,
amount,
{ gasLimit: 4_000_000 }
)
TxUnsigned.chainId = BigInt(31337) // Hardcoded chainId for Hardhat local testing
Expand Down

0 comments on commit 4b819f4

Please sign in to comment.