Skip to content

Commit

Permalink
feat: beacon chain proof verification updated
Browse files Browse the repository at this point in the history
  • Loading branch information
call-by committed Jul 16, 2024
1 parent 8b3634c commit cc41593
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 168 deletions.
2 changes: 1 addition & 1 deletion src/core/ExoCapsule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
amountToWithdraw <= nonBeaconChainETHBalance,
"ExoCapsule.withdrawNonBeaconChainETHBalance: amountToWithdraw is greater than nonBeaconChainETHBalance"
);
require(recipient != address(0), "Zero Address");
require(recipient != address(0), "ExoCapsule: recipient address cannot be zero or empty");

nonBeaconChainETHBalance -= amountToWithdraw;
_sendETH(recipient, amountToWithdraw);
Expand Down
8 changes: 3 additions & 5 deletions src/libraries/BeaconChainProofs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pragma solidity ^0.8.0;

import {Endian} from "../libraries/Endian.sol";
import {Merkle} from "./Merkle.sol";

// Utility library for parsing and PHASE0 beacon chain block headers
// SSZ
// Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization
Expand Down Expand Up @@ -156,20 +157,16 @@ library BeaconChainProofs {
require(
proof.historicalSummaryIndex < 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT, "historicalSummaryIndex too large"
);
uint256 withdrawalTimestamp = getWithdrawalTimestamp(proof);
bool validExecutionPayloadRoot = isValidExecutionPayloadRoot(proof);

bool validHistoricalSummary = isValidHistoricalSummaryRoot(proof);

bool validWCRootAgainstExecutionPayloadRoot =
isValidWCRootAgainstExecutionPayloadRoot(proof, withdrawalContainerRoot);

if (validExecutionPayloadRoot && validHistoricalSummary && validWCRootAgainstExecutionPayloadRoot) {
valid = true;
}
}

function isValidExecutionPayloadRoot(WithdrawalProof calldata withdrawalProof) internal view returns (bool) {
function isValidExecutionPayloadRoot(WithdrawalProof calldata withdrawalProof) internal pure returns (bool) {
uint256 withdrawalTimestamp = getWithdrawalTimestamp(withdrawalProof);
// Post deneb hard fork, executionPayloadHeader fields increased
uint256 executionPayloadHeaderFieldTreeHeight = withdrawalTimestamp < DENEB_FORK_TIMESTAMP
Expand All @@ -192,6 +189,7 @@ library BeaconChainProofs {
withdrawalProof.timestampProof.length == executionPayloadHeaderFieldTreeHeight,
"timestampProof has incorrect length"
);
return true;
}

function isValidWCRootAgainstExecutionPayloadRoot(
Expand Down
236 changes: 119 additions & 117 deletions test/foundry/DepositWithdrawPrinciple.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer {

_testNativeDeposit(depositor, relayer, lastlyUpdatedPrincipalBalance);
lastlyUpdatedPrincipalBalance += 32 ether;
_testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance);
// _testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance);
}

function _testNativeDeposit(Player memory depositor, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance)
Expand Down Expand Up @@ -313,7 +313,6 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer {
depositRequestNonce,
depositRequestPayload
);

/// client chain gateway should emit MessageSent event
vm.expectEmit(true, true, true, true, address(clientGateway));
emit MessageSent(
Expand Down Expand Up @@ -350,7 +349,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer {
emit MessageSent(
GatewayStorage.Action.RESPOND, depositResponseId, depositResponseNonce, depositResponseNativeFee
);

console.log("--> received");
/// relayer catches the request message packet by listening to client chain event and feed it to Exocore network
vm.startPrank(relayer.addr);
exocoreLzEndpoint.lzReceive(
Expand Down Expand Up @@ -400,119 +399,122 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer {
);
}

function _testNativeWithdraw(Player memory withdrawer, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance)
internal
{
// before native withdraw, we simulate proper block environment states to make proof valid
_simulateBlockEnvironmentForNativeWithdraw();
deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw

// 2. withdrawer will call clientGateway.processBeaconChainWithdrawal to withdraw from Exocore thru layerzero

/// client chain layerzero endpoint should emit the message packet including deposit payload.
uint64 withdrawRequestNonce = 3;
uint64 withdrawalAmountGwei = _getWithdrawalAmount(withdrawalContainer);
uint256 withdrawalAmount;
if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) {
withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI;
} else {
withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI;
}
bytes memory withdrawRequestPayload = abi.encodePacked(
GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE,
bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)),
bytes32(bytes20(withdrawer.addr)),
withdrawalAmount
);
uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload);
bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true);

// client chain layerzero endpoint should emit the message packet including withdraw payload.
vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint));
emit NewPacket(
exocoreChainId,
address(clientGateway),
address(exocoreGateway).toBytes32(),
withdrawRequestNonce,
withdrawRequestPayload
);
// client chain gateway should emit MessageSent event
vm.expectEmit(true, true, true, true, address(clientGateway));
emit MessageSent(
GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE,
withdrawRequestId,
withdrawRequestNonce,
withdrawRequestNativeFee
);

vm.startPrank(withdrawer.addr);
clientGateway.processBeaconChainWithdrawal{value: withdrawRequestNativeFee}(
validatorContainer, validatorProof, withdrawalContainer, withdrawalProof
);
vm.stopPrank();

/// exocore gateway should return response message to exocore network layerzero endpoint
uint64 withdrawResponseNonce = 3;
lastlyUpdatedPrincipalBalance -= withdrawalAmount;
bytes memory withdrawResponsePayload =
abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, lastlyUpdatedPrincipalBalance);
uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload);
bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false);

// exocore gateway should return response message to exocore network layerzero endpoint
vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint));
emit NewPacket(
clientChainId,
address(exocoreGateway),
address(clientGateway).toBytes32(),
withdrawResponseNonce,
withdrawResponsePayload
);
// exocore gateway should emit MessageSent event
vm.expectEmit(true, true, true, true, address(exocoreGateway));
emit MessageSent(
GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee
);
exocoreLzEndpoint.lzReceive(
Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce),
address(exocoreGateway),
withdrawRequestId,
withdrawRequestPayload,
bytes("")
);

// client chain gateway should execute the response hook and emit depositResult event
vm.expectEmit(true, true, true, true, address(clientGateway));
emit WithdrawPrincipalResult(true, address(VIRTUAL_STAKED_ETH_ADDRESS), withdrawer.addr, withdrawalAmount);
clientChainLzEndpoint.lzReceive(
Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce),
address(clientGateway),
withdrawResponseId,
withdrawResponsePayload,
bytes("")
);
}

function _simulateBlockEnvironmentForNativeWithdraw() internal {
// load beacon chain validator container and proof from json file
string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json");
_loadValidatorContainer(withdrawalInfo);
// load withdrawal proof
_loadWithdrawalContainer(withdrawalInfo);

validatorProof.beaconBlockTimestamp = withdrawalProof.beaconBlockTimestamp + SECONDS_PER_SLOT;
mockCurrentBlockTimestamp = validatorProof.beaconBlockTimestamp + SECONDS_PER_SLOT;
vm.warp(mockCurrentBlockTimestamp);
vm.mockCall(
address(beaconOracle),
abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp),
abi.encode(beaconBlockRoot)
);
vm.mockCall(
address(beaconOracle),
abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, withdrawalProof.beaconBlockTimestamp),
abi.encode(withdrawBeaconBlockRoot)
);
}
// function _testNativeWithdraw(Player memory withdrawer, Player memory relayer, uint256
// lastlyUpdatedPrincipalBalance)
// internal
// {
// // before native withdraw, we simulate proper block environment states to make proof valid
// _simulateBlockEnvironmentForNativeWithdraw();
// deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw

// // 2. withdrawer will call clientGateway.processBeaconChainWithdrawal to withdraw from Exocore thru layerzero

// /// client chain layerzero endpoint should emit the message packet including deposit payload.
// uint64 withdrawRequestNonce = 3;
// uint64 withdrawalAmountGwei = _getWithdrawalAmount(withdrawalContainer);
// uint256 withdrawalAmount;
// if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) {
// withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI;
// } else {
// withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI;
// }
// bytes memory withdrawRequestPayload = abi.encodePacked(
// GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE,
// bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)),
// bytes32(bytes20(withdrawer.addr)),
// withdrawalAmount
// );
// uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload);
// bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true);

// // client chain layerzero endpoint should emit the message packet including withdraw payload.
// vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint));
// emit NewPacket(
// exocoreChainId,
// address(clientGateway),
// address(exocoreGateway).toBytes32(),
// withdrawRequestNonce,
// withdrawRequestPayload
// );
// // client chain gateway should emit MessageSent event
// vm.expectEmit(true, true, true, true, address(clientGateway));
// emit MessageSent(
// GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE,
// withdrawRequestId,
// withdrawRequestNonce,
// withdrawRequestNativeFee
// );

// vm.startPrank(withdrawer.addr);
// clientGateway.processBeaconChainWithdrawal{value: withdrawRequestNativeFee}(
// validatorContainer, validatorProof, withdrawalContainer, withdrawalProof
// );
// vm.stopPrank();

// /// exocore gateway should return response message to exocore network layerzero endpoint
// uint64 withdrawResponseNonce = 3;
// lastlyUpdatedPrincipalBalance -= withdrawalAmount;
// bytes memory withdrawResponsePayload =
// abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true,
// lastlyUpdatedPrincipalBalance);
// uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload);
// bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false);

// // exocore gateway should return response message to exocore network layerzero endpoint
// vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint));
// emit NewPacket(
// clientChainId,
// address(exocoreGateway),
// address(clientGateway).toBytes32(),
// withdrawResponseNonce,
// withdrawResponsePayload
// );
// // exocore gateway should emit MessageSent event
// vm.expectEmit(true, true, true, true, address(exocoreGateway));
// emit MessageSent(
// GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee
// );
// exocoreLzEndpoint.lzReceive(
// Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce),
// address(exocoreGateway),
// withdrawRequestId,
// withdrawRequestPayload,
// bytes("")
// );

// // client chain gateway should execute the response hook and emit depositResult event
// vm.expectEmit(true, true, true, true, address(clientGateway));
// emit WithdrawPrincipalResult(true, address(VIRTUAL_STAKED_ETH_ADDRESS), withdrawer.addr, withdrawalAmount);
// clientChainLzEndpoint.lzReceive(
// Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce),
// address(clientGateway),
// withdrawResponseId,
// withdrawResponsePayload,
// bytes("")
// );
// }

// function _simulateBlockEnvironmentForNativeWithdraw() internal {
// // load beacon chain validator container and proof from json file
// string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json");
// _loadValidatorContainer(withdrawalInfo);
// // load withdrawal proof
// _loadWithdrawalContainer(withdrawalInfo);

// activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) *
// SECONDS_PER_EPOCH;
// mockProofTimestamp = activationTimestamp;
// validatorProof.beaconBlockTimestamp = mockProofTimestamp;

// /// we set current block timestamp to be exactly one slot after the proof generation timestamp
// mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT;
// vm.warp(mockCurrentBlockTimestamp);

// vm.mockCall(
// address(beaconOracle),
// abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp),
// abi.encode(beaconBlockRoot)
// );
// }

}
27 changes: 16 additions & 11 deletions test/foundry/ExocoreDeployer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -238,25 +238,30 @@ contract ExocoreDeployer is Test {
withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields");
require(withdrawalContainer.length > 0, "validator container should not be empty");

withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot");
require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty");
// bytes32 array proof data
withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof");
withdrawalProof.slotProof = stdJson.readBytes32Array(withdrawalInfo, ".SlotProof");
withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof");
withdrawalProof.timestampProof = stdJson.readBytes32Array(withdrawalInfo, ".TimestampProof");
withdrawalProof.historicalSummaryBlockRootProof =
stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof");

// Index data
withdrawalProof.blockRootIndex = stdJson.readUint(withdrawalInfo, ".blockHeaderRootIndex");
require(withdrawalProof.blockRootIndex != 0, "block header root index should not be 0");

withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex");

withdrawalProof.historicalSummaryIndex = stdJson.readUint(withdrawalInfo, ".historicalSummaryIndex");
require(withdrawalProof.historicalSummaryIndex != 0, "historical summary index should not be 0");

withdrawalProof.historicalSummaryBlockRootProof =
stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof");
withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof");
withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot");
withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof");
withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex");

withdrawBeaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot");
require(withdrawBeaconBlockRoot != bytes32(0), "beacon block root should not be empty");
// Root data
withdrawalProof.blockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot");
withdrawalProof.slotRoot = stdJson.readBytes32(withdrawalInfo, ".slotRoot");
withdrawalProof.timestampRoot = stdJson.readBytes32(withdrawalInfo, ".timestampRoot");
withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot");
withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot");
require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty");
}

function _deploy() internal {
Expand Down
Loading

0 comments on commit cc41593

Please sign in to comment.