Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions ethexe/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@

[dependencies]
alloy-primitives = { workspace = true, features = ["serde"] }
binary-merkle-tree.workspace = true
gear-core.workspace = true
sp-core.workspace = true
gprimitives.workspace = true
parity-scale-codec.workspace = true
scale-info = { workspace = true, features = ["derive"] }
hex.workspace = true
serde = { workspace = true, optional = true }
sp-runtime.workspace = true
sp-io = { workspace = true, features = ["disable_allocator"] }

Check failure on line 21 in ethexe/common/Cargo.toml

View workflow job for this annotation

GitHub Actions / check / unused-deps

shear/unused_dependency

unused dependency `sp-io` (remove this dependency)
roast-secp256k1-evm.workspace = true
derive_more.workspace = true
anyhow.workspace = true
Expand Down
10 changes: 9 additions & 1 deletion ethexe/common/src/gear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use parity_scale_codec::{Decode, Encode};
use roast_secp256k1_evm::frost::keys::VerifiableSecretSharingCommitment;
use scale_info::TypeInfo;
use sha3::Digest as _;
use sp_runtime::traits::Keccak256 as SpRuntimeKeccak256;

// TODO: support query from router.
pub const COMPUTATION_THRESHOLD: u64 = 2_500_000_000;
Expand Down Expand Up @@ -397,13 +398,20 @@ impl ToDigest for StateTransition {
messages,
} = self;

let value_claims: Vec<_> = value_claims
.iter()
.map(|value_claim| H256(value_claim.to_digest().0))
.collect();
let value_claims_merkle_root =
binary_merkle_tree::merkle_root_raw::<SpRuntimeKeccak256, _>(value_claims);

hasher.update(actor_id.to_address_lossy());
hasher.update(new_state_hash);
hasher.update([*exited as u8]);
hasher.update(inheritor.to_address_lossy());
hasher.update(value_to_receive.to_be_bytes());
hasher.update([*value_to_receive_negative_sign as u8]);
hasher.update(value_claims.to_digest());
hasher.update(value_claims_merkle_root);
hasher.update(messages.to_digest());
}
}
Expand Down
50 changes: 49 additions & 1 deletion ethexe/contracts/src/IMirror.sol
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,21 @@ interface IMirror {
*/
error TransferLockedValueToInheritorExternalFailed();

/**
* @dev Thrown when the value claimId (messageId) is already processed.
*/
error ValueClaimAlreadyProcessed(bytes32 messageId);

/**
* @dev Thrown when the merkle root is not found for the state hash in MessageQueue smart contract.
*/
error ValueClaimMerkleRootNotFound(bytes32 stateHash);

/**
* @dev Thrown when the merkle proof is invalid for value claims.
*/
error ValueClaimInvalidMerkleProof();

error InitializerAlreadySet();

error IsSmallAlreadySet();
Expand All @@ -219,7 +234,7 @@ interface IMirror {

/* # Functions section */

/* # Operational functions */
/* # View functions */

/**
* @dev Returns the address of the `Router` contract, which is the sole authority
Expand Down Expand Up @@ -255,6 +270,21 @@ interface IMirror {
*/
function initializer() external view returns (address);

/**
* @dev Returns the value claims merkle root for the specified state hash.
* Returns `bytes32(0)` if no merkle root was provided for the given state hash.
* @param stateHash Target state hash.
* @return merkleRoot Value claims merkle root for the specified state hash.
*/
function getValueClaimsMerkleRoot(bytes32 stateHash) external view returns (bytes32);

/**
* @dev Checks if value claim was already processed.
* @param messageId Message ID to check.
* @return isProcessed `true` if value claim was already processed, `false` otherwise.
*/
function isValueClaimProcessed(bytes32 messageId) external view returns (bool);

/* # Primary Gear logic (external calls) */

/**
Expand Down Expand Up @@ -311,6 +341,24 @@ interface IMirror {
*/
function transferLockedValueToInheritor() external;

/* # Primary Gear logic (external calls, pull-based methods) */

/**
* @dev Claims value from the message in the mailbox.
* @param stateHash The state hash for which to claim value.
* @param totalLeaves The total number of leaves in the merkle tree.
* @param leafIndex The index of the leaf for which to claim value.
* @param claim The value claim data.
* @param proof The merkle proof for the claim.
*/
function claimValue(
bytes32 stateHash,
uint256 totalLeaves,
uint256 leafIndex,
Gear.ValueClaim calldata claim,
bytes32[] calldata proof
) external;

/* # Router-driven state and funds management */

/**
Expand Down
103 changes: 71 additions & 32 deletions ethexe/contracts/src/Mirror.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {ICallbacks} from "src/ICallbacks.sol";
import {IMirror} from "src/IMirror.sol";
import {IRouter} from "src/IRouter.sol";
import {IWrappedVara} from "src/IWrappedVara.sol";
import {BinaryMerkleTree} from "src/libraries/BinaryMerkleTree.sol";
import {Gear} from "src/libraries/Gear.sol";

/**
Expand Down Expand Up @@ -128,6 +129,9 @@ contract Mirror is IMirror {
*/
bool isSmall;

mapping(bytes32 stateHash => bytes32 merkleRoot) private _valueClaimsMerkleRoots;
mapping(bytes32 messageId => bool isProcessed) private _processedValueClaims;

/**
* @dev Minimal constructor that only sets the immutable `Router` address.
* @param _router The address of the `Router` contract.
Expand Down Expand Up @@ -256,6 +260,27 @@ contract Mirror is IMirror {
}
}

/* # View functions */

/**
* @dev Returns the value claims merkle root for the specified state hash.
* Returns `bytes32(0)` if no merkle root was provided for the given state hash.
* @param _stateHash Target state hash.
* @return merkleRoot Value claims merkle root for the specified state hash.
*/
function getValueClaimsMerkleRoot(bytes32 _stateHash) external view returns (bytes32) {
return _valueClaimsMerkleRoots[_stateHash];
}

/**
* @dev Checks if value claim was already processed.
* @param _messageId Message ID to check.
* @return isProcessed `true` if value claim was already processed, `false` otherwise.
*/
function isValueClaimProcessed(bytes32 _messageId) external view returns (bool) {
return _processedValueClaims[_messageId];
}

/* # Primary Gear logic (external calls) */

/**
Expand Down Expand Up @@ -348,6 +373,44 @@ contract Mirror is IMirror {
require(success, TransferLockedValueToInheritorExternalFailed());
}

/* # Primary Gear logic (external calls, pull-based methods) */

/**
* @dev Claims value from the message in the mailbox.
* @param _stateHash The state hash for which to claim value.
* @param _totalLeaves The total number of leaves in the merkle tree.
* @param _leafIndex The index of the leaf for which to claim value.
* @param _claim The value claim data.
* @param _proof The merkle proof for the claim.
*/
function claimValue(
bytes32 _stateHash,
uint256 _totalLeaves,
uint256 _leafIndex,
Gear.ValueClaim calldata _claim,
bytes32[] calldata _proof
) external {
require(!_processedValueClaims[_claim.messageId], ValueClaimAlreadyProcessed(_claim.messageId));

bytes32 merkleRoot = _valueClaimsMerkleRoots[_stateHash];
require(merkleRoot != bytes32(0), ValueClaimMerkleRootNotFound(_stateHash));

bytes32 claimHash = Gear.valueClaimHash(_claim.messageId, _claim.destination, _claim.value);
require(
BinaryMerkleTree.verifyProofCalldata(merkleRoot, _proof, _totalLeaves, _leafIndex, claimHash),
ValueClaimInvalidMerkleProof()
);

_processedValueClaims[_claim.messageId] = true;

bool success = _transferEther(_claim.destination, _claim.value);
if (success) {
emit ValueClaimed(_claim.messageId, _claim.value);
} else {
emit ValueClaimFailed(_claim.messageId, _claim.value);
}
Comment on lines +404 to +411
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In the new pull-based claimValue function, the claim is marked as processed (_processedValueClaims[_claim.messageId] = true) even if the ether transfer fails. Since the user initiates this call, they should be able to retry if the transfer fails (e.g., if the destination is a contract that temporarily reverts or runs out of gas). Marking it as processed on failure permanently locks the funds in the contract. It is recommended to use require(success, ...) to revert the transaction on failure, which ensures the state is not updated and allows for retries.

        _processedValueClaims[_claim.messageId] = true;

        bool success = _transferEther(_claim.destination, _claim.value);
        require(success, "Transfer failed");

        emit ValueClaimed(_claim.messageId, _claim.value);

}

/* # Router-driven state and funds management */

/**
Expand Down Expand Up @@ -412,9 +475,9 @@ contract Mirror is IMirror {
bytes32 messagesHashesHash = _sendMessages(_transition.messages);

/**
* @dev Send value for each claim.
* @dev Sets merkle root of value claims for the new state hash.
*/
bytes32 valueClaimsHash = _claimValues(_transition.valueClaims);
_claimValues(_transition.newStateHash, _transition.valueClaimsMerkleRoot);

/**
* @dev Set inheritor if exited.
Expand Down Expand Up @@ -442,7 +505,7 @@ contract Mirror is IMirror {
_transition.inheritor,
_transition.valueToReceive,
_transition.valueToReceiveNegativeSign,
valueClaimsHash,
_transition.valueClaimsMerkleRoot,
messagesHashesHash
);
}
Expand Down Expand Up @@ -923,36 +986,12 @@ contract Mirror is IMirror {

// TODO (breathx): claimValues will fail if the program is exited: keep the funds on `Router`.
/**
* @dev Internal function to claim values from messages in mailbox.
* It transfers value to each claim destination and emits appropriate events:
* - `ValueClaimed` event is emitted if transfer is successful
* - `ValueClaimFailed` event is emitted if transfer fails
* @param _claims The array of value claims to be claimed.
* @return claimsHash The hash of the claimed values.
* @dev Internal function to pass claim values from messages in mailbox as merkle root.
* @param _stateHash The state hash for which the values are claimed.
* @param _valueClaimsMerkleRoot The merkle root of value claims for the state hash.
*/
function _claimValues(Gear.ValueClaim[] calldata _claims) private returns (bytes32 claimsHash) {
uint256 claimsLen = _claims.length;
uint256 claimsHashesSize = claimsLen * 32;
uint256 claimsHashesMemPtr = Memory.allocate(claimsHashesSize);
uint256 offset = 0;

for (uint256 i = 0; i < claimsLen; i++) {
Gear.ValueClaim calldata claim = _claims[i];
bytes32 claimHash = Gear.valueClaimHash(claim.messageId, claim.destination, claim.value);
Memory.writeWordAsBytes32(claimsHashesMemPtr, offset, claimHash);
unchecked {
offset += 32;
}

bool success = _transferEther(claim.destination, claim.value);
if (success) {
emit ValueClaimed(claim.messageId, claim.value);
} else {
emit ValueClaimFailed(claim.messageId, claim.value);
}
}

return Hashes.efficientKeccak256AsBytes32(claimsHashesMemPtr, 0, claimsHashesSize);
function _claimValues(bytes32 _stateHash, bytes32 _valueClaimsMerkleRoot) private {
_valueClaimsMerkleRoots[_stateHash] = _valueClaimsMerkleRoot;
}

// TODO (breathx): allow zero inheritor in `Router`.
Expand Down
Loading
Loading