Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ pgo-data.profdata
.env

# Generated inputs
/script/examples/*
/script/examples/*

CLAUDE.md
31 changes: 31 additions & 0 deletions Cargo.lock

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

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["primitives", "script", "program"]
members = ["primitives", "script", "program", "test"]
resolver = "2"

[workspace.package]
Expand Down Expand Up @@ -38,6 +38,11 @@ clap = "4.5.9"
log = "0.4.22"
env_logger = "0.11.3"
alloy-primitives = "0.8.15"
alloy-trie = "0.7.9"
alloy-rlp = { version = "0.3.9", default-features = false, features = [
"derive",
"arrayvec",
] }
alloy = { version = "0.9.1", features = ["full"] }
anyhow = "1.0.86"
reqwest = "0.12.5"
Expand All @@ -52,4 +57,4 @@ sha3-v0-10-8 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", packa
tiny-keccak = { git = "https://github.com/sp1-patches/tiny-keccak", tag = "patch-2.0.2-sp1-4.0.0" }
bls12_381 = { git = "https://github.com/sp1-patches/bls12_381", tag = "patch-0.8.0-sp1-4.0.0" }
# From upstream: https://github.com/a16z/helios/blob/master/Cargo.toml#L115
ethereum_hashing = { git = "https://github.com/ncitron/ethereum_hashing", rev = "7ee70944ed4fabe301551da8c447e4f4ae5e6c35" }
ethereum_hashing = { git = "https://github.com/ncitron/ethereum_hashing", rev = "7ee70944ed4fabe301551da8c447e4f4ae5e6c35" }
22 changes: 11 additions & 11 deletions contracts/genesis.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"executionStateRoot": "0xdf692e41511ac4a0382f73ea44b3820bd87effc050d43c104abca9bc482c250b",
"genesisTime": 1655733600,
"genesisValidatorsRoot": "0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078",
"guardian": "0x788c45cafab3ea427b9079889be43d7d3cd7500c",
"head": 6733152,
"header": "0xce795ebea5c29436d4a10ca097712d2785e4f8541caec377c7388306d6c21376",
"heliosProgramVkey": "0x00796ec46c244906ad25c835f76c682e538e7ec0837a77bdaa88f0eb1aad8e9f",
"executionStateRoot": "0xfe4cb76cb3d629d8c73f0421b399416549d61382d49c749f6bef8e17f13a415b",
"genesisTime": 1606824023,
"genesisValidatorsRoot": "0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95",
"guardian": "0x77349640ef922e919d14e24692be1ee38dbfde54",
"head": 11349408,
"header": "0x3262d85ccc58ea8d41b69c779bfa5672ab34058a251bfd4b4ea8cd90122c7914",
"heliosProgramVkey": "0x005b36529a91cf26aca076f004673c457efbbd34ccb19bf4d8f90d395e17027c",
"secondsPerSlot": 12,
"slotsPerEpoch": 32,
"slotsPerPeriod": 8192,
"sourceChainId": 11155111,
"syncCommitteeHash": "0x5aaad4aeaf4c830b38bfe4f9f5fe3cfcdad919639b871ea20cf1e7eff30a8a36",
"verifier": "0x3b6041173b80e77f038f3f2c0f9744f04837185e"
}
"sourceChainId": 1,
"syncCommitteeHash": "0xff83cc067aad82f208b045a9313f09036b5b55b9f67ea0dd2bf5d7eaa8bdee7f",
"verifier": "0x397a5f7f3dbd538f23de225b51f532c34448da9b"
}
139 changes: 122 additions & 17 deletions contracts/src/SP1Helios.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ contract SP1Helios {
uint256 public immutable SLOTS_PER_EPOCH;
uint256 public immutable SOURCE_CHAIN_ID;

/// @notice Maximum number of time behind current timestamp for a block to be used for proving
/// @dev This is set to 1 week to prevent timing attacks where malicious validators
/// could retroactively create forks that diverge from the canonical chain. To minimize this
/// risk, we limit the maximum age of a block to 1 week.
uint256 public constant MAX_SLOT_AGE = 1 weeks;

modifier onlyGuardian() {
require(msg.sender == guardian, "Caller is not the guardian");
_;
}

/// @notice The latest slot the light client has a finalized header for.
uint256 public head = 0;
uint256 public head;

/// @notice Maps from a slot to a beacon block header root.
mapping(uint256 => bytes32) public headers;
Expand All @@ -30,6 +36,9 @@ contract SP1Helios {
/// @notice Maps from a period to the hash for the sync committee.
mapping(uint256 => bytes32) public syncCommittees;

/// @notice Maps from (block number, contract, slot) tuple to storage value
mapping(bytes32 => bytes32) public storageValues;

/// @notice The verification key for the SP1 Helios program.
bytes32 public heliosProgramVkey;

Expand All @@ -39,6 +48,12 @@ contract SP1Helios {
/// @notice The address of the guardian
address public guardian;

struct StorageSlot {
bytes32 key;
bytes32 value;
address contractAddress;
}

struct ProofOutputs {
bytes32 executionStateRoot;
bytes32 newHeader;
Expand All @@ -50,6 +65,7 @@ contract SP1Helios {
bytes32 syncCommitteeHash;
// Hash of the current sync committee that signed the previous update.
bytes32 startSyncCommitteeHash;
StorageSlot[] slots;
}

struct InitParams {
Expand All @@ -70,12 +86,20 @@ contract SP1Helios {

event HeadUpdate(uint256 indexed slot, bytes32 indexed root);
event SyncCommitteeUpdate(uint256 indexed period, bytes32 indexed root);
event StorageSlotVerified(
uint256 indexed slot,
bytes32 indexed key,
bytes32 value,
address contractAddress
);

error SlotBehindHead(uint256 slot);
error SyncCommitteeAlreadySet(uint256 period);
error HeaderRootAlreadySet(uint256 slot);
error StateRootAlreadySet(uint256 slot);
error InvalidHeaderRoot(uint256 slot);
error InvalidStateRoot(uint256 slot);
error SyncCommitteeStartMismatch(bytes32 given, bytes32 expected);
error PreviousHeadNotSet(uint256 slot);
error PreviousHeadTooOld(uint256 slot);

constructor(InitParams memory params) {
GENESIS_VALIDATORS_ROOT = params.genesisValidatorsRoot;
Expand All @@ -84,7 +108,8 @@ contract SP1Helios {
SLOTS_PER_PERIOD = params.slotsPerPeriod;
SLOTS_PER_EPOCH = params.slotsPerEpoch;
SOURCE_CHAIN_ID = params.sourceChainId;
syncCommittees[getSyncCommitteePeriod(params.head)] = params.syncCommitteeHash;
syncCommittees[getSyncCommitteePeriod(params.head)] = params
.syncCommitteeHash;
heliosProgramVkey = params.heliosProgramVkey;
headers[params.head] = params.header;
executionStateRoots[params.head] = params.executionStateRoot;
Expand All @@ -96,42 +121,89 @@ contract SP1Helios {
/// @notice Updates the light client with a new header, execution state root, and sync committee (if changed)
/// @param proof The proof bytes for the SP1 proof.
/// @param publicValues The public commitments from the SP1 proof.
function update(bytes calldata proof, bytes calldata publicValues) external {
/// @param fromHead The head slot to prove against.
function update(
bytes calldata proof,
bytes calldata publicValues,
uint256 fromHead
) external {
if (headers[fromHead] == bytes32(0)) {
revert PreviousHeadNotSet(fromHead);
}

// Check if the head being proved against is older than allowed.
if (block.timestamp - slotTimestamp(fromHead) > MAX_SLOT_AGE) {
revert PreviousHeadTooOld(fromHead);
}

// Parse the outputs from the committed public values associated with the proof.
ProofOutputs memory po = abi.decode(publicValues, (ProofOutputs));
if (po.newHead <= head) {
if (po.newHead < fromHead) {
revert SlotBehindHead(po.newHead);
}

uint256 currentPeriod = getSyncCommitteePeriod(head);
uint256 currentPeriod = getSyncCommitteePeriod(fromHead);

// Note: We should always have a sync committee for the current head.
// The "start" sync committee hash is the hash of the sync committee that should sign the next update.
bytes32 currentSyncCommitteeHash = syncCommittees[currentPeriod];
if (currentSyncCommitteeHash != po.startSyncCommitteeHash) {
revert SyncCommitteeStartMismatch(po.startSyncCommitteeHash, currentSyncCommitteeHash);
revert SyncCommitteeStartMismatch(
po.startSyncCommitteeHash,
currentSyncCommitteeHash
);
}

// Verify the proof with the associated public values. This will revert if proof invalid.
ISP1Verifier(verifier).verifyProof(heliosProgramVkey, publicValues, proof);
ISP1Verifier(verifier).verifyProof(
heliosProgramVkey,
publicValues,
proof
);

// Check that the new header hasnt been set already.
head = po.newHead;
if (headers[po.newHead] != bytes32(0)) {
revert HeaderRootAlreadySet(po.newHead);
if (
headers[po.newHead] != bytes32(0) &&
headers[po.newHead] != po.newHeader
) {
revert InvalidHeaderRoot(po.newHead);
}
// Set new header.
headers[po.newHead] = po.newHeader;
if (head < po.newHead) {
head = po.newHead;
}

// Check that the new state root hasnt been set already.
headers[po.newHead] = po.newHeader;
if (executionStateRoots[po.newHead] != bytes32(0)) {
revert StateRootAlreadySet(po.newHead);
if (
executionStateRoots[po.newHead] != bytes32(0) &&
executionStateRoots[po.newHead] != po.executionStateRoot
) {
revert InvalidStateRoot(po.newHead);
}

// Finally set the new state root.
executionStateRoots[po.newHead] = po.executionStateRoot;
emit HeadUpdate(po.newHead, po.newHeader);

uint256 period = getSyncCommitteePeriod(head);
// Store all provided storage slot values
for (uint256 i = 0; i < po.slots.length; i++) {
StorageSlot memory slot = po.slots[i];
bytes32 storageKey = computeStorageKey(
po.newHead,
slot.contractAddress,
slot.key
);
storageValues[storageKey] = slot.value;
emit StorageSlotVerified(
po.newHead,
slot.key,
slot.value,
slot.contractAddress
);
}

uint256 period = getSyncCommitteePeriod(po.newHead);

// If the sync committee for the new peroid is not set, set it.
// This can happen if the light client was very behind and had a lot of updates
Expand All @@ -158,7 +230,9 @@ contract SP1Helios {
}

/// @notice Gets the sync committee period from a slot.
function getSyncCommitteePeriod(uint256 slot) public view returns (uint256) {
function getSyncCommitteePeriod(
uint256 slot
) public view returns (uint256) {
return slot / SLOTS_PER_PERIOD;
}

Expand All @@ -171,4 +245,35 @@ contract SP1Helios {
function updateHeliosProgramVkey(bytes32 newVkey) external onlyGuardian {
heliosProgramVkey = newVkey;
}

/// @notice Gets the timestamp of a slot
function slotTimestamp(uint256 slot) public view returns (uint256) {
return GENESIS_TIME + slot * SECONDS_PER_SLOT;
}

/// @notice Gets the timestamp of the latest head
function headTimestamp() public view returns (uint256) {
return slotTimestamp(head);
}

/// @notice Computes the key for a contract's storage slot
function computeStorageKey(
uint256 blockNumber,
address contractAddress,
bytes32 slot
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(blockNumber, contractAddress, slot));
}

/// @notice Gets the value of a storage slot at a specific block
function getStorageSlot(
uint256 blockNumber,
address contractAddress,
bytes32 slot
) external view returns (bytes32) {
return
storageValues[
computeStorageKey(blockNumber, contractAddress, slot)
];
}
}
Binary file modified elf/sp1-helios-elf
Binary file not shown.
1 change: 1 addition & 0 deletions primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ serde = { workspace = true }
helios-consensus-core = { workspace = true }
alloy-sol-types = { workspace = true }
alloy-primitives = { workspace = true }
alloy-trie = { workspace = true }
Loading