> For the complete documentation index, see [llms.txt](https://docs.astralbeam.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.astralbeam.io/smart-contracts/casper-eth-client.md).

# Casper EthClient

The EthClient contract stores EVM block headers on Casper Network and provides verification services for cross-chain proofs.

## Overview

| Property | Value                   |
| -------- | ----------------------- |
| Language | Rust (Odra Framework)   |
| License  | MIT                     |
| Storage  | Per-chain block headers |
| Security | M-of-N Multi-signature  |

## Core Concepts

### Multi-Chain Storage

The EthClient stores block headers from multiple EVM chains using composite keys:

```rust
// Storage key: (chain_id, block_number)
block_headers: Mapping<(u64, u64), BlockHeader>

// Examples:
// (11155111, 1000000) → Sepolia block 1,000,000
// (84532, 1000000)    → Base block 1,000,000
```

This prevents block collisions between chains with the same block numbers.

### Block Header Structure

```rust
struct BlockHeader {
    block_hash: H256,
    state_root: H256,
    receipts_root: H256,
}
```

## Core Functions

### submit\_block\_header\_with\_signatures

Submit a block header with M-of-N relayer signatures.

```rust
pub fn submit_block_header_with_signatures(
    &mut self,
    chain_id: u64,
    block_number: u64,
    block_hash: H256,
    state_root: H256,
    receipts_root: H256,
    signatures: Vec<Vec<u8>>,
)
```

**Parameters:**

| Name            | Type      | Description                       |
| --------------- | --------- | --------------------------------- |
| `chain_id`      | u64       | EVM chain identifier              |
| `block_number`  | u64       | Block number                      |
| `block_hash`    | H256      | 32-byte block hash                |
| `state_root`    | H256      | 32-byte state root                |
| `receipts_root` | H256      | 32-byte receipts root             |
| `signatures`    | Vec\<Vec> | Array of 65-byte ECDSA signatures |

**Process:**

1. Compute attestation hash (includes chain\_id)
2. Verify M-of-N signatures from registered relayers
3. Store block header with composite key
4. Update latest block for chain

**Attestation Hash:**

```rust
// Hash includes chain_id FIRST for replay protection
let hash = keccak256(
    chain_id.to_be_bytes() +
    block_number.to_be_bytes() +
    block_hash +
    state_root +
    receipts_root
);
```

**Events:**

```rust
struct BlockHeaderSubmitted {
    block_number: u64,
    block_hash: H256,
    submitter: Address,
}
```

***

### get\_block\_hash

Get the stored block hash for a chain/block.

```rust
pub fn get_block_hash(&self, chain_id: u64, block_number: u64) -> Option<H256>
```

**Returns:**

* `Some(hash)` if block exists
* `None` if block not stored

***

### get\_state\_root

Get the stored state root.

```rust
pub fn get_state_root(&self, chain_id: u64, block_number: u64) -> Option<H256>
```

***

### get\_receipts\_root

Get the stored receipts root for MPT verification.

```rust
pub fn get_receipts_root(&self, chain_id: u64, block_number: u64) -> Option<H256>
```

**Usage:** TokenFactory uses this to verify lock event proofs:

```rust
let stored_root = eth_client.get_receipts_root(chain_id, block_number);
verify_mpt_proof(stored_root, proof, expected_receipt);
```

***

### is\_block\_known

Check if a block is stored.

```rust
pub fn is_block_known(&self, chain_id: u64, block_number: u64) -> bool
```

***

### get\_latest\_block

Get the latest stored block for a chain.

```rust
pub fn get_latest_block(&self, chain_id: u64) -> u64
```

***

## Admin Functions

### add\_eth\_relayer

Add a new relayer.

```rust
pub fn add_eth_relayer(&mut self, eth_address: H160)
```

**Access:** Admin only

**Reverts** with `InvalidThreshold` if the grown active set would break the strict-majority rule (e.g. adding a 4th relayer to a 2-of-3 config — 2-of-4 is exactly half, not a majority). To grow past the boundary, raise the threshold with `set_threshold` first, then add.

**Events:**

```rust
struct RelayerAdded {
    eth_address: H160,
    relayer_index: u32,
}
```

***

### remove\_eth\_relayer

Remove a relayer.

```rust
pub fn remove_eth_relayer(&mut self, eth_address: H160)
```

**Access:** Admin only

**Events:**

```rust
struct RelayerRemoved {
    eth_address: H160,
}
```

***

### set\_threshold

Update the signature threshold.

```rust
pub fn set_threshold(&mut self, new_threshold: u32)
```

**Access:** Admin only

**Validation:**

* `new_threshold >= 2`
* `new_threshold <= active_relayer_count`
* `new_threshold * 2 > active_relayer_count` (strict majority — 1-of-N and other minority quorums are rejected with `InvalidThreshold`)

The same rule applies at `init` and on `remove_eth_relayer` (a removal that would leave the stored threshold invalid for the shrunken set reverts).

**Events:**

```rust
struct ThresholdUpdated {
    old_threshold: u32,
    new_threshold: u32,
}
```

***

### propose\_admin

Propose a new admin (step 1 of the two-step transfer). Rejects the zero address and the current admin. Overwrites any previous pending proposal.

```rust
pub fn propose_admin(&mut self, new_admin: Address)
```

**Access:** Admin only

**Events:** `AdminTransferProposed`

***

### accept\_admin

Accept a pending admin transfer (step 2 of 2). The admin role only moves once the proposed admin calls this with their own key — a mistyped address can never take the role.

```rust
pub fn accept_admin(&mut self)
```

**Access:** Proposed (pending) admin only

**Events:** `AdminTransferAccepted`

***

## View Functions

### get\_threshold

Get current signature threshold.

```rust
pub fn get_threshold(&self) -> u32
```

### get\_relayer\_count

Get total number of relayers.

```rust
pub fn get_relayer_count(&self) -> u32
```

### is\_eth\_relayer

Check if address is a relayer.

```rust
pub fn is_eth_relayer(&self, eth_address: H160) -> bool
```

### get\_eth\_relayer

Get relayer address by index.

```rust
pub fn get_eth_relayer(&self, index: u32) -> Option<H160>
```

### get\_authorized\_relayers

Get all authorized relayer addresses.

```rust
pub fn get_authorized_relayers(&self) -> Vec<H160>
```

***

## Error Codes

| Error                           | Code  | Description                                         |
| ------------------------------- | ----- | --------------------------------------------------- |
| `Unauthorized`                  | 60001 | Caller is not the admin                             |
| `BlockAlreadySubmitted`         | 60003 | Block header already submitted for this chain/block |
| `BlockNotFound`                 | 60004 | Block header not found in storage                   |
| `InvalidBlockNumber`            | 60005 | Invalid block number provided                       |
| `InsufficientSignatures`        | 60006 | Not enough valid signatures (below threshold)       |
| `InvalidSignature`              | 60007 | Signature format invalid or recovery failed         |
| `UnauthorizedRelayer`           | 60008 | Recovered signer is not an authorized relayer       |
| `InvalidThreshold`              | 60009 | Threshold must be > 0 and <= relayer count          |
| `RelayerAlreadyExists`          | 60010 | Relayer address already registered                  |
| `RelayerNotFound`               | 60011 | Relayer address not found in registry               |
| `AttestationVerificationFailed` | 60012 | Attestation hash verification failed                |

***

## Storage Schema

```rust
struct EthClient {
    // Administration
    admin: Address,

    // Relayer management
    relayer_addresses: Mapping<u32, H160>,
    is_authorized: Mapping<H160, bool>,
    relayer_count: u32,
    threshold: u32,

    // Block storage (per-chain)
    block_headers: Mapping<(u64, u64), BlockHeader>,  // (chain_id, block_number)
    latest_blocks: Mapping<u64, u64>,                  // chain_id → latest_block
}
```

***

## Signature Verification

### Hash Computation

The attestation hash includes chain\_id as the FIRST element to prevent cross-chain replay:

```rust
pub fn compute_block_header_attestation_hash(
    chain_id: u64,
    block_number: u64,
    block_hash: H256,
    state_root: H256,
    receipts_root: H256,
) -> H256 {
    let mut data = Vec::with_capacity(104); // 8 + 8 + 32 + 32 + 32

    data.extend_from_slice(&chain_id.to_be_bytes());      // 8 bytes
    data.extend_from_slice(&block_number.to_be_bytes()); // 8 bytes
    data.extend_from_slice(&block_hash);                  // 32 bytes
    data.extend_from_slice(&state_root);                  // 32 bytes
    data.extend_from_slice(&receipts_root);               // 32 bytes

    keccak256(&data)
}
```

### ECDSA Recovery

```rust
pub fn verify_eth_signature(
    message_hash: &[u8; 32],
    signature: &[u8; 65],  // r (32) + s (32) + v (1)
    expected_address: &[u8; 20],
) -> bool {
    // Extract r, s, v
    let r = &signature[0..32];
    let s = &signature[32..64];
    let v = signature[64];

    // Recover public key from signature
    let recovered_pubkey = secp256k1_recover(message_hash, r, s, v);

    // Derive Ethereum address from public key
    let recovered_address = keccak256(&recovered_pubkey[1..])[12..32];

    recovered_address == expected_address
}
```

***

## Integration Examples

### TypeScript (via bridge-cli)

```typescript
import { CasperClient } from './casper';

const client = new CasperClient(config);

// Submit block header
await client.submitBlockHeaderAttestation({
    chainId: 11155111,
    blockNumber: 5000000n,
    blockHash: '0x...',
    stateRoot: '0x...',
    receiptsRoot: '0x...',
}, signatures);

// Query block header
const receiptsRoot = await client.getReceiptsRoot(11155111, 5000000n);
const isKnown = await client.isBlockKnown(11155111, 5000000n);
const latestBlock = await client.getLatestBlock(11155111);
```

### Query via casper-client

```bash
# Check if block is known
casper-client query-global-state \
  --node-address https://node.testnet.casper.network \
  --key "hash-<eth_client_hash>" \
  --query-path "block_headers/(11155111, 5000000)"
```

***

## Security Considerations

1. **Chain ID in Hash**: Prevents replay of signatures across chains
2. **M-of-N Verification**: Requires threshold signatures
3. **Relayer Whitelist**: Only registered relayers can sign
4. **No Overwrites**: Submitted blocks cannot be modified
5. **Independent Storage**: Each chain's blocks stored separately

***

## Multi-Chain Security

### Why Chain ID Matters

Without chain ID in the attestation hash:

```
Attack: Use Sepolia signature to submit fake Base block
   - Same block number on both chains
   - Signatures would be valid without chain_id
```

With chain ID:

```
Safe: Signatures are chain-specific
   - Hash(Sepolia, 1000000, ...) ≠ Hash(Base, 1000000, ...)
   - Sepolia signatures invalid for Base blocks
```

### Block Number Isolation

```
Sepolia block 1,000,000 stored at: (11155111, 1000000)
Base block 1,000,000 stored at:    (84532, 1000000)

Different keys → No collision
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.astralbeam.io/smart-contracts/casper-eth-client.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
