Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contracts-communication): Interchain versioning library #2389

Merged
merged 17 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
53 changes: 37 additions & 16 deletions packages/contracts-communication/contracts/InterchainClientV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "./libs/InterchainTransaction.sol";
import {OptionsLib, OptionsV1} from "./libs/Options.sol";
import {TypeCasts} from "./libs/TypeCasts.sol";
import {VersionedPayloadLib} from "./libs/VersionedPayload.sol";

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

Expand All @@ -26,6 +27,10 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainClientV1 {
using AppConfigLib for bytes;
using OptionsLib for bytes;
using VersionedPayloadLib for bytes;

/// @notice Version of the InterchainClient contract. Sent and received transactions must have the same version.
uint16 public constant CLIENT_VERSION = 1;

/// @notice Address of the InterchainDB contract, set at the time of deployment.
address public immutable INTERCHAIN_DB;
Expand Down Expand Up @@ -97,8 +102,9 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli
external
payable
{
InterchainTransaction memory icTx = InterchainTransactionLib.decodeTransaction(transaction);
bytes32 transactionId = _assertExecutable(icTx, proof);
InterchainTransaction memory icTx = _assertCorrectVersion(transaction);
bytes32 transactionId = keccak256(transaction);
_assertExecutable(icTx, transactionId, proof);
_txExecutor[transactionId] = msg.sender;

OptionsV1 memory decodedOptions = icTx.options.decodeOptionsV1();
Expand Down Expand Up @@ -136,17 +142,17 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli

// @inheritdoc IInterchainClientV1
function isExecutable(bytes calldata encodedTx, bytes32[] calldata proof) external view returns (bool) {
InterchainTransaction memory icTx = InterchainTransactionLib.decodeTransaction(encodedTx);
InterchainTransaction memory icTx = _assertCorrectVersion(encodedTx);
// Check that options could be decoded
icTx.options.decodeOptionsV1();
_assertExecutable(icTx, proof);
bytes32 transactionId = keccak256(encodedTx);
_assertExecutable(icTx, transactionId, proof);
return true;
}

// @inheritdoc IInterchainClientV1
function getExecutor(bytes calldata encodedTx) external view returns (address) {
InterchainTransaction memory icTx = InterchainTransactionLib.decodeTransaction(encodedTx);
return _txExecutor[icTx.transactionId()];
return _txExecutor[keccak256(encodedTx)];
}

// @inheritdoc IInterchainClientV1
Expand Down Expand Up @@ -199,16 +205,19 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli
}
}

/// @notice Encodes the transaction data into a bytes format.
function encodeTransaction(InterchainTransaction memory icTx) external pure returns (bytes memory) {
return icTx.encodeTransaction();
}

/// @notice Decodes the encoded options data into a OptionsV1 struct.
function decodeOptions(bytes memory encodedOptions) external pure returns (OptionsV1 memory) {
function decodeOptions(bytes memory encodedOptions) external view returns (OptionsV1 memory) {
return encodedOptions.decodeOptionsV1();
}

/// @notice Encodes the transaction data into a bytes format.
function encodeTransaction(InterchainTransaction memory icTx) public pure returns (bytes memory) {
return VersionedPayloadLib.encodeVersionedPayload({
version: CLIENT_VERSION,
payload: InterchainTransactionLib.encodeTransaction(icTx)
});
}

// ═════════════════════════════════════════════════ INTERNAL ══════════════════════════════════════════════════════

/// @dev Internal logic for sending a message to another chain.
Expand Down Expand Up @@ -241,7 +250,7 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli
options: options,
message: message
});
desc.transactionId = icTx.transactionId();
desc.transactionId = keccak256(encodeTransaction(icTx));
// Sanity check: nonce returned from DB should match the nonce used to construct the transaction
{
(uint256 dbNonce, uint64 entryIndex) = IInterchainDB(INTERCHAIN_DB).writeEntryWithVerification{
Expand Down Expand Up @@ -284,20 +293,19 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli

// ══════════════════════════════════════════════ INTERNAL VIEWS ═══════════════════════════════════════════════════

/// @dev Asserts that the transaction is executable. Returns the transactionId for chaining purposes.
/// @dev Asserts that the transaction is executable.
function _assertExecutable(
InterchainTransaction memory icTx,
bytes32 transactionId,
bytes32[] calldata proof
)
internal
view
returns (bytes32 transactionId)
{
bytes32 linkedClient = _assertLinkedClient(icTx.srcChainId);
if (icTx.dstChainId != block.chainid) {
revert InterchainClientV1__IncorrectDstChainId(icTx.dstChainId);
}
transactionId = icTx.transactionId();
if (_txExecutor[transactionId] != address(0)) {
revert InterchainClientV1__TxAlreadyExecuted(transactionId);
}
Expand Down Expand Up @@ -357,4 +365,17 @@ contract InterchainClientV1 is Ownable, InterchainClientV1Events, IInterchainCli
}
}
}

/// @dev Asserts that the transaction version is correct. Returns the decoded transaction for chaining purposes.
function _assertCorrectVersion(bytes calldata versionedTx)
internal
pure
returns (InterchainTransaction memory icTx)
{
uint16 version = versionedTx.getVersion();
if (version != CLIENT_VERSION) {
revert InterchainClientV1__InvalidTransactionVersion(version);
}
icTx = InterchainTransactionLib.decodeTransaction(versionedTx.getPayload());
}
}
22 changes: 19 additions & 3 deletions packages/contracts-communication/contracts/InterchainDB.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {IInterchainModule} from "./interfaces/IInterchainModule.sol";

import {InterchainBatch, InterchainBatchLib} from "./libs/InterchainBatch.sol";
import {InterchainEntry, InterchainEntryLib} from "./libs/InterchainEntry.sol";
import {TypeCasts} from "./libs/TypeCasts.sol";
import {VersionedPayloadLib} from "./libs/VersionedPayload.sol";

contract InterchainDB is InterchainDBEvents, IInterchainDB {
using VersionedPayloadLib for bytes;

uint16 public constant DB_VERSION = 1;

bytes32[] internal _entryValues;
mapping(address module => mapping(bytes32 batchKey => RemoteBatch batch)) internal _remoteBatches;

Expand Down Expand Up @@ -63,7 +67,15 @@ contract InterchainDB is InterchainDBEvents, IInterchainDB {
// ═══════════════════════════════════════════════ MODULE-FACING ═══════════════════════════════════════════════════

/// @inheritdoc IInterchainDB
function verifyRemoteBatch(InterchainBatch memory batch) external onlyRemoteChainId(batch.srcChainId) {
function verifyRemoteBatch(bytes calldata versionedBatch) external {
uint16 dbVersion = versionedBatch.getVersion();
if (dbVersion != DB_VERSION) {
revert InterchainDB__InvalidBatchVersion(dbVersion);
}
InterchainBatch memory batch = InterchainBatchLib.decodeBatch(versionedBatch.getPayload());
if (batch.srcChainId == block.chainid) {
revert InterchainDB__SameChainId(batch.srcChainId);
}
bytes32 batchKey = InterchainBatchLib.batchKey(batch);
RemoteBatch memory existingBatch = _remoteBatches[msg.sender][batchKey];
// Check if that's the first time module verifies the batch
Expand Down Expand Up @@ -209,8 +221,12 @@ contract InterchainDB is InterchainDBEvents, IInterchainDB {
fees[0] += msg.value - totalFee;
}
uint256 len = srcModules.length;
bytes memory versionedBatch = VersionedPayloadLib.encodeVersionedPayload({
version: DB_VERSION,
payload: InterchainBatchLib.encodeBatch(batch)
});
for (uint256 i = 0; i < len; ++i) {
IInterchainModule(srcModules[i]).requestBatchVerification{value: fees[i]}(dstChainId, batch);
IInterchainModule(srcModules[i]).requestBatchVerification{value: fees[i]}(dstChainId, versionedBatch);
}
emit InterchainBatchVerificationRequested(dstChainId, batch.dbNonce, batch.batchRoot, srcModules);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {IExecutionService, ISynapseExecutionServiceV1} from "../interfaces/ISyna
import {SynapseExecutionServiceEvents} from "../events/SynapseExecutionServiceEvents.sol";
import {IGasOracle} from "../interfaces/IGasOracle.sol";
import {OptionsLib, OptionsV1} from "../libs/Options.sol";
import {VersionedPayloadLib} from "../libs/VersionedPayload.sol";

import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";

Expand Down Expand Up @@ -70,7 +71,7 @@ contract SynapseExecutionServiceV1 is
uint256 txPayloadSize,
bytes32 transactionId,
uint256 executionFee,
bytes memory options
bytes calldata options
)
external
virtual
Expand All @@ -87,7 +88,7 @@ contract SynapseExecutionServiceV1 is
function getExecutionFee(
uint256 dstChainId,
uint256 txPayloadSize,
bytes memory options
bytes calldata options
)
public
view
Expand All @@ -98,8 +99,9 @@ contract SynapseExecutionServiceV1 is
if (cachedGasOracle == address(0)) {
revert SynapseExecutionService__GasOracleNotSet();
}
// TODO: the "exact version" check should be generalized
(uint8 version,) = OptionsLib.decodeVersionedOptions(options);
// ExecutionServiceV1 implementation only supports Options V1.
// Following versions will be supported by the future implementations.
uint16 version = VersionedPayloadLib.getVersion(options);
if (version > OptionsLib.OPTIONS_V1) {
revert SynapseExecutionService__OptionsVersionNotSupported(version);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface IInterchainClientV1 {
error InterchainClientV1__FeeAmountTooLow(uint256 actual, uint256 required);
error InterchainClientV1__IncorrectDstChainId(uint256 chainId);
error InterchainClientV1__IncorrectMsgValue(uint256 actual, uint256 required);
error InterchainClientV1__InvalidTransactionVersion(uint16 version);
error InterchainClientV1__NoLinkedClient(uint256 chainId);
error InterchainClientV1__NotEnoughResponses(uint256 actual, uint256 required);
error InterchainClientV1__NotEVMClient(bytes32 client);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface IInterchainDB {
error InterchainDB__ConflictingBatches(address module, bytes32 existingBatchRoot, InterchainBatch newBatch);
error InterchainDB__EntryIndexOutOfRange(uint256 dbNonce, uint64 entryIndex, uint64 batchSize);
error InterchainDB__IncorrectFeeAmount(uint256 actualFee, uint256 expectedFee);
error InterchainDB__InvalidBatchVersion(uint16 version);
error InterchainDB__InvalidEntryRange(uint256 dbNonce, uint64 start, uint64 end);
error InterchainDB__NoModulesSpecified();
error InterchainDB__SameChainId(uint256 chainId);
Expand Down Expand Up @@ -68,8 +69,9 @@ interface IInterchainDB {
returns (uint256 dbNonce, uint64 entryIndex);

/// @notice Allows the Interchain Module to verify the batch coming from the remote chain.
/// @param batch The Interchain Batch to confirm
function verifyRemoteBatch(InterchainBatch memory batch) external;
/// Note: The DB will only accept the batch of the same version as the DB itself.
/// @param versionedBatch The versioned Interchain Batch to verify
function verifyRemoteBatch(bytes memory versionedBatch) external;

// ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

/// @notice Every Module may opt a different method to confirm the verified entries on destination chain,
/// therefore this is not a part of a common interface.
interface IInterchainModule {
Expand All @@ -19,9 +16,9 @@ interface IInterchainModule {
/// Note: this will eventually trigger `InterchainDB.verifyRemoteBatch(batch)` function on destination chain,
/// with no guarantee of ordering.
/// @dev Could be only called by the Interchain DataBase contract.
/// @param dstChainId The chain id of the destination chain
/// @param batch The batch to verify
function requestBatchVerification(uint256 dstChainId, InterchainBatch memory batch) external payable;
/// @param dstChainId The chain id of the destination chain
/// @param versionedBatch The versioned batch to verify
function requestBatchVerification(uint256 dstChainId, bytes memory versionedBatch) external payable;

/// @notice Get the Module fee for verifying a batch on the specified destination chain.
/// @param dstChainId The chain id of the destination chain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {IExecutionService} from "./IExecutionService.sol";
interface ISynapseExecutionServiceV1 is IExecutionService {
error SynapseExecutionService__GasOracleNotSet();
error SynapseExecutionService__FeeAmountTooLow(uint256 actual, uint256 required);
error SynapseExecutionService__OptionsVersionNotSupported(uint256 version);
error SynapseExecutionService__OptionsVersionNotSupported(uint16 version);
error SynapseExecutionService__ZeroAddress();

/// @notice Allows the contract governor to set the address of the EOA account that will be used
Expand Down
43 changes: 14 additions & 29 deletions packages/contracts-communication/contracts/libs/AppConfig.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {VersionedPayloadLib} from "./VersionedPayload.sol";

struct AppConfigV1 {
uint256 requiredResponses;
uint256 optimisticPeriod;
Expand All @@ -9,45 +11,28 @@ struct AppConfigV1 {
using AppConfigLib for AppConfigV1 global;

library AppConfigLib {
uint8 constant APP_CONFIG_V1 = 1;

error AppConfigLib__IncorrectVersion(uint8 version);
using VersionedPayloadLib for bytes;

/// @notice Encodes versioned app config into a bytes format.
/// @param version The version of the app config.
/// @param appConfig The app config to encode.
function encodeVersionedAppConfig(uint8 version, bytes memory appConfig) internal pure returns (bytes memory) {
return abi.encode(version, appConfig);
}
uint16 internal constant APP_CONFIG_V1 = 1;

/// @notice Decodes versioned app config from a bytes format back into a version and app config.
/// @param data The versioned app config data in bytes format.
/// @return version The version of the app config.
/// @return appConfig The app config as bytes.
function decodeVersionedAppConfig(bytes memory data)
internal
pure
returns (uint8 version, bytes memory appConfig)
{
(version, appConfig) = abi.decode(data, (uint8, bytes));
}

/// @notice Encodes V1 app config into a bytes format.
/// @param appConfig The AppConfigV1 to encode.
function encodeAppConfigV1(AppConfigV1 memory appConfig) internal pure returns (bytes memory) {
return encodeVersionedAppConfig(APP_CONFIG_V1, abi.encode(appConfig));
}
error AppConfigLib__IncorrectVersion(uint16 version);

/// @notice Decodes app config (V1 or higher) from a bytes format back into an AppConfigV1 struct.
/// @param data The app config data in bytes format.
function decodeAppConfigV1(bytes memory data) internal pure returns (AppConfigV1 memory) {
(uint8 version, bytes memory appConfig) = decodeVersionedAppConfig(data);
function decodeAppConfigV1(bytes memory data) internal view returns (AppConfigV1 memory) {
uint16 version = data.getVersionFromMemory();
if (version < APP_CONFIG_V1) {
revert AppConfigLib__IncorrectVersion(version);
}
// Structs of the same version will always be decoded correctly.
// Following versions will be decoded correctly if they have the same fields as the previous version,
// and new fields at the end: abi.decode ignores the extra bytes in the decoded payload.
return abi.decode(appConfig, (AppConfigV1));
return abi.decode(data.getPayloadFromMemory(), (AppConfigV1));
}

/// @notice Encodes V1 app config into a bytes format.
/// @param appConfig The AppConfigV1 to encode.
function encodeAppConfigV1(AppConfigV1 memory appConfig) internal pure returns (bytes memory) {
return VersionedPayloadLib.encodeVersionedPayload(APP_CONFIG_V1, abi.encode(appConfig));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {VersionedPayloadLib} from "./VersionedPayload.sol";

/// @notice Struct representing a batch of entries in the Interchain DataBase.
/// Batched entries are put together in a Merkle tree, which root is saved.
/// Batch has a globally unique identifier (key) and a value.
Expand All @@ -17,6 +19,8 @@ struct InterchainBatch {
}

library InterchainBatchLib {
using VersionedPayloadLib for bytes;

/// @notice Constructs an InterchainBatch struct to be saved on the local chain.
/// @param dbNonce The database nonce of the batch
/// @param batchRoot The root of the Merkle tree containing the batched entries
Expand All @@ -32,6 +36,21 @@ library InterchainBatchLib {
return InterchainBatch({srcChainId: block.chainid, dbNonce: dbNonce, batchRoot: batchRoot});
}

/// @notice Encodes the InterchainBatch struct into a non-versioned batch payload.
function encodeBatch(InterchainBatch memory batch) internal pure returns (bytes memory) {
return abi.encode(batch);
}

/// @notice Decodes the InterchainBatch struct from a non-versioned batch payload in calldata.
function decodeBatch(bytes calldata data) internal pure returns (InterchainBatch memory) {
return abi.decode(data, (InterchainBatch));
}

/// @notice Decodes the InterchainBatch struct from a non-versioned batch payload in memory.
function decodeBatchFromMemory(bytes memory data) internal pure returns (InterchainBatch memory) {
return abi.decode(data, (InterchainBatch));
}

/// @notice Returns the globally unique identifier of the batch
function batchKey(InterchainBatch memory batch) internal pure returns (bytes32) {
return keccak256(abi.encode(batch.srcChainId, batch.dbNonce));
Expand Down
Loading
Loading