Skip to content

Commit

Permalink
Feat: interchain check batch verification (#2521)
Browse files Browse the repository at this point in the history
* Scaffold new verification function

* Update DB tests

* Update ClientV1 test

* Feat: check batch verification

* Test: ClientV1 should revert on incorrect entryIndex/proof

* Expose generic `getEntryValue`

* Use `checkBatchVerification` in Client

* Rm unused harness

* Deprecate `checkVerification`

* Cleanup: isolate batch root logic

* Scaffold new getter in `InterchainDB`

* Specify unit tests for the new getter

* Implement batch root getter

* Cleanup: isolate entry + proof -> batch root logic

* Cleanup: tests

* chore: InterchainDB interface clean up
  • Loading branch information
ChiTimesChi authored Apr 22, 2024
1 parent fe52a70 commit fa2e1a0
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 236 deletions.
25 changes: 14 additions & 11 deletions packages/contracts-communication/contracts/InterchainClientV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {IInterchainClientV1} from "./interfaces/IInterchainClientV1.sol";
import {IInterchainDB} from "./interfaces/IInterchainDB.sol";

import {AppConfigV1, AppConfigLib} from "./libs/AppConfig.sol";
import {InterchainEntry} from "./libs/InterchainEntry.sol";
import {BatchingV1Lib} from "./libs/BatchingV1.sol";
import {InterchainBatch} from "./libs/InterchainBatch.sol";
import {
InterchainTransaction, InterchainTxDescriptor, InterchainTransactionLib
} from "./libs/InterchainTransaction.sol";
Expand Down Expand Up @@ -324,21 +325,24 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli
if (_txExecutor[transactionId] != address(0)) {
revert InterchainClientV1__TxAlreadyExecuted(transactionId);
}
// Construct expected entry based on icTransaction data
InterchainEntry memory icEntry = InterchainEntry({
// Construct expected batch based on interchain transaction data
InterchainBatch memory batch = InterchainBatch({
srcChainId: icTx.srcChainId,
dbNonce: icTx.dbNonce,
entryIndex: icTx.entryIndex,
srcWriter: linkedClient,
dataHash: transactionId
batchRoot: BatchingV1Lib.getBatchRoot({
srcWriter: linkedClient,
dataHash: transactionId,
entryIndex: icTx.entryIndex,
proof: proof
})
});
(bytes memory encodedAppConfig, address[] memory approvedDstModules) =
IInterchainApp(TypeCasts.bytes32ToAddress(icTx.dstReceiver)).getReceivingConfig();
AppConfigV1 memory appConfig = encodedAppConfig.decodeAppConfigV1();
if (appConfig.requiredResponses == 0) {
revert InterchainClientV1__ZeroRequiredResponses();
}
uint256 responses = _getFinalizedResponsesCount(approvedDstModules, icEntry, proof, appConfig.optimisticPeriod);
uint256 responses = _getFinalizedResponsesCount(approvedDstModules, batch, appConfig.optimisticPeriod);
if (responses < appConfig.requiredResponses) {
revert InterchainClientV1__NotEnoughResponses(responses, appConfig.requiredResponses);
}
Expand All @@ -358,22 +362,21 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli
/**
* @dev Calculates the number of responses that are considered finalized within the optimistic time period.
* @param approvedModules Approved modules that could have confirmed the entry.
* @param icEntry The InterchainEntry to confirm.
* @param batch The Interchain Batch to confirm.
* @param optimisticPeriod The time period in seconds within which a response is considered valid.
* @return finalizedResponses The count of responses that are finalized within the optimistic time period.
*/
function _getFinalizedResponsesCount(
address[] memory approvedModules,
InterchainEntry memory icEntry,
bytes32[] calldata proof,
InterchainBatch memory batch,
uint256 optimisticPeriod
)
internal
view
returns (uint256 finalizedResponses)
{
for (uint256 i = 0; i < approvedModules.length; ++i) {
uint256 confirmedAt = IInterchainDB(INTERCHAIN_DB).checkVerification(approvedModules[i], icEntry, proof);
uint256 confirmedAt = IInterchainDB(INTERCHAIN_DB).checkBatchVerification(approvedModules[i], batch);
// checkVerification() returns 0 if entry hasn't been confirmed by the module, so we check for that as well
if (confirmedAt != 0 && confirmedAt + optimisticPeriod < block.timestamp) {
++finalizedResponses;
Expand Down
44 changes: 26 additions & 18 deletions packages/contracts-communication/contracts/InterchainDB.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {InterchainDBEvents} from "./events/InterchainDBEvents.sol";
import {IInterchainDB} from "./interfaces/IInterchainDB.sol";
import {IInterchainModule} from "./interfaces/IInterchainModule.sol";

import {BatchingV1Lib} from "./libs/BatchingV1.sol";
import {InterchainBatch, InterchainBatchLib, BatchKey} from "./libs/InterchainBatch.sol";
import {InterchainEntry, InterchainEntryLib} from "./libs/InterchainEntry.sol";
import {VersionedPayloadLib} from "./libs/VersionedPayload.sol";
Expand All @@ -13,6 +14,15 @@ import {TypeCasts} from "./libs/TypeCasts.sol";
contract InterchainDB is InterchainDBEvents, IInterchainDB {
using VersionedPayloadLib for bytes;

/// @notice Struct representing a batch of entries from the remote Interchain DataBase,
/// verified by the Interchain Module.
/// @param verifiedAt The block timestamp at which the entry was verified by the module
/// @param batchRoot The Merkle root of the batch
struct RemoteBatch {
uint256 verifiedAt;
bytes32 batchRoot;
}

uint16 public constant DB_VERSION = 1;

bytes32[] internal _entryValues;
Expand Down Expand Up @@ -132,31 +142,29 @@ contract InterchainDB is InterchainDBEvents, IInterchainDB {
}

/// @inheritdoc IInterchainDB
function checkVerification(
function checkBatchVerification(
address dstModule,
InterchainEntry memory entry,
bytes32[] calldata proof
InterchainBatch memory batch
)
external
view
onlyRemoteChainId(entry.srcChainId)
onlyRemoteChainId(batch.srcChainId)
returns (uint256 moduleVerifiedAt)
{
// In "no batching" mode: the batch root is the same as the entry value, hence the proof is empty
if (proof.length != 0) {
// If proof is not empty, the batch root is not verified
return 0;
}
// In "no batching" mode: entry index is 0, batch size is 1
if (entry.entryIndex != 0) {
// If entry index is not 0, it does not belong to the batch
return 0;
}
BatchKey batchKey = InterchainBatchLib.encodeBatchKey({srcChainId: entry.srcChainId, dbNonce: entry.dbNonce});
BatchKey batchKey = InterchainBatchLib.encodeBatchKey({srcChainId: batch.srcChainId, dbNonce: batch.dbNonce});
RemoteBatch memory remoteBatch = _remoteBatches[dstModule][batchKey];
bytes32 entryValue = InterchainEntryLib.entryValue(entry);
// Check entry value against the batch root verified by the module
return remoteBatch.batchRoot == entryValue ? remoteBatch.verifiedAt : 0;
// Check batch root against the batch root verified by the module
return remoteBatch.batchRoot == batch.batchRoot ? remoteBatch.verifiedAt : 0;
}

/// @inheritdoc IInterchainDB
function getBatchRoot(InterchainEntry memory entry, bytes32[] calldata proof) external pure returns (bytes32) {
return BatchingV1Lib.getBatchRoot({
srcWriter: entry.srcWriter,
dataHash: entry.dataHash,
entryIndex: entry.entryIndex,
proof: proof
});
}

/// @inheritdoc IInterchainDB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
import {InterchainTxDescriptor} from "../libs/InterchainTransaction.sol";

interface IInterchainClientV1 {
// TODO: standardize error names across interfaces
error InterchainClientV1__FeeAmountTooLow(uint256 actual, uint256 required);
error InterchainClientV1__IncorrectDstChainId(uint64 chainId);
error InterchainClientV1__IncorrectMsgValue(uint256 actual, uint256 required);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {InterchainEntry} from "../libs/InterchainEntry.sol";
import {InterchainBatch} from "../libs/InterchainBatch.sol";
import {InterchainEntry} from "../libs/InterchainEntry.sol";

interface IInterchainDB {
/// @notice Struct representing a batch of entries from the remote Interchain DataBase,
/// verified by the Interchain Module.
/// @param verifiedAt The block timestamp at which the entry was verified by the module
/// @param batchRoot The Merkle root of the batch
struct RemoteBatch {
uint256 verifiedAt;
bytes32 batchRoot;
}

error InterchainDB__BatchDoesNotExist(uint64 dbNonce);
error InterchainDB__BatchNotFinalized(uint64 dbNonce);
error InterchainDB__ConflictingBatches(address module, bytes32 existingBatchRoot, InterchainBatch newBatch);
Expand Down Expand Up @@ -144,22 +135,25 @@ interface IInterchainDB {
/// @return entryIndex The index of the next entry within that batch
function getNextEntryIndex() external view returns (uint64 dbNonce, uint64 entryIndex);

/// @notice Read the data written on specific source chain by a specific writer,
/// and verify it on the destination chain using the provided Interchain Module.
/// Note: returned zero value indicates that the module has not verified the entry.
/// @param entry The Interchain Entry to read
/// @notice Check if the batch is verified by the Interchain Module on the destination chain.
/// Note: returned zero value indicates that the module has not verified the batch.
/// @param dstModule The destination chain addresses of the Interchain Modules to use for verification
/// @return moduleVerifiedAt The block timestamp at which the entry was verified by the module,
/// or ZERO if the module has not verified the entry.
function checkVerification(
/// @param batch The Interchain Batch to check
/// @return moduleVerifiedAt The block timestamp at which the batch was verified by the module,
/// or ZERO if the module has not verified the batch.
function checkBatchVerification(
address dstModule,
InterchainEntry memory entry,
bytes32[] memory proof
InterchainBatch memory batch
)
external
view
returns (uint256 moduleVerifiedAt);

/// @notice Get the batch root containing the Interchain Entry with the given index.
/// @param entry The Interchain Entry to get the batch root for
/// @param proof The Merkle proof of inclusion for the entry in the batch
function getBatchRoot(InterchainEntry memory entry, bytes32[] memory proof) external pure returns (bytes32);

/// @notice Get the version of the Interchain DataBase.
// solhint-disable-next-line func-name-mixedcase
function DB_VERSION() external pure returns (uint16);
Expand Down
36 changes: 36 additions & 0 deletions packages/contracts-communication/contracts/libs/BatchingV1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {InterchainEntryLib} from "./InterchainEntry.sol";

library BatchingV1Lib {
error BatchingV1__IncorrectEntryIndex(uint64 entryIndex);
error BatchingV1__IncorrectProof();

/// @notice Get the batch root containing the Interchain Entry with the given index.
/// @param srcWriter The entry writer of the source chain
/// @param dataHash The hash of the data of the entry
/// @param entryIndex The index of the entry in the batch
/// @param proof The Merkle proof of inclusion for the entry in the batch
/// @return batchRoot The root of the batch containing the entry
function getBatchRoot(
bytes32 srcWriter,
bytes32 dataHash,
uint64 entryIndex,
bytes32[] calldata proof
)
internal
pure
returns (bytes32 batchRoot)
{
// In "no batching" mode: entry index is 0, proof is empty
if (entryIndex != 0) {
revert BatchingV1__IncorrectEntryIndex(entryIndex);
}
if (proof.length != 0) {
revert BatchingV1__IncorrectProof();
}
// In "no batching" mode: the batch root is the same as the entry value
return InterchainEntryLib.getEntryValue({srcWriter: srcWriter, dataHash: dataHash});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ library InterchainEntryLib {

/// @notice Returns the value of the entry: writer + dataHash hashed together
function entryValue(InterchainEntry memory entry) internal pure returns (bytes32) {
return keccak256(abi.encode(entry.srcWriter, entry.dataHash));
return getEntryValue(entry.srcWriter, entry.dataHash);
}

/// @notice Returns the value of the entry: writer + dataHash hashed together
function getEntryValue(bytes32 srcWriter, bytes32 dataHash) internal pure returns (bytes32) {
return keccak256(abi.encode(srcWriter, dataHash));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@
pragma solidity 0.8.20;

import {InterchainClientV1, InterchainClientV1Events, IInterchainClientV1} from "../contracts/InterchainClientV1.sol";
import {
InterchainTxDescriptor,
InterchainTransaction,
InterchainTransactionLib
} from "../contracts/libs/InterchainTransaction.sol";
import {InterchainTxDescriptor, InterchainTransaction} from "../contracts/libs/InterchainTransaction.sol";
import {BatchingV1Lib} from "../contracts/libs/BatchingV1.sol";
import {OptionsLib} from "../contracts/libs/Options.sol";

import {InterchainTransactionLibHarness} from "./harnesses/InterchainTransactionLibHarness.sol";
Expand Down Expand Up @@ -80,12 +77,20 @@ abstract contract InterchainClientV1BaseTest is Test, InterchainClientV1Events {
);
}

function expectRevertIncorrectEntryIndex(uint64 entryIndex) internal {
vm.expectRevert(abi.encodeWithSelector(BatchingV1Lib.BatchingV1__IncorrectEntryIndex.selector, entryIndex));
}

function expectRevertIncorrectMsgValue(uint256 actual, uint256 required) internal {
vm.expectRevert(
abi.encodeWithSelector(IInterchainClientV1.InterchainClientV1__IncorrectMsgValue.selector, actual, required)
);
}

function expectRevertIncorrectProof() internal {
vm.expectRevert(BatchingV1Lib.BatchingV1__IncorrectProof.selector);
}

function expectRevertInvalidTransactionVersion(uint16 version) internal {
vm.expectRevert(
abi.encodeWithSelector(IInterchainClientV1.InterchainClientV1__InvalidTransactionVersion.selector, version)
Expand Down
Loading

0 comments on commit fa2e1a0

Please sign in to comment.