For the complete documentation index, see llms.txt. This page is also available as Markdown.
hkdf
On-chain HKDF-SHA256 key derivation
Derive cryptographic keys on-chain using Mercury EVM's HKDF precompile.
Overview
The HKDF precompile at address 0x68 performs HKDF-SHA256 key derivation on input key material (IKM). It produces a uniformly distributed 32-byte derived key suitable for use as an AES-256 key or other cryptographic purpose.
The SeismicSignedProvider uses this precompile internally to derive AES keys from ECDH shared secrets during the TEE key exchange. You can also call it directly for custom key derivation workflows.
Precompile Address
0x0000000000000000000000000000000000000068
Input Encoding
Field
Size
Description
ikm
Variable
Input key material (arbitrary bytes)
The input is the raw IKM bytes — no additional encoding is needed.
Output Format
Field
Size
Description
derived_key
32 bytes
HKDF-SHA256 derived key
Parameters
Parameter
Type
Required
Description
ikm
&[u8]
Yes
Input key material (arbitrary bytes)
Examples
Basic Usage
Derive Multiple Keys by Context
Use as AES Key
Derive from ECDH Output
Deterministic Key Derivation
How It Works
Encode parameters — Passes input key material as-is
Call precompile — Issues an eth_call to address 0x68 with gas proportional to input length
HKDF derivation — Precompile performs HKDF-SHA256 extract and expand phases
Return key — Returns first 32 bytes of derived key material
Gas Cost
Gas cost is calculated as:
For example:
IKM Size
Gas Cost
32 bytes
~6144
64 bytes
~6168
128 bytes
~6216
Notes
Uses HKDF-SHA256 from RFC 5869
Always returns exactly 32 bytes regardless of input length
Input key material can be any length
The derivation is deterministic: same IKM always produces the same output
Internally performs both HKDF-Extract and HKDF-Expand phases
The derived key has uniform distribution suitable for cryptographic use
Use Cases
Derive encryption keys from shared secrets
Convert non-uniform entropy into uniform keys
Key separation: derive multiple keys from one master secret
Post-process ECDH output for additional security
Warnings
Not for password hashing — Use proper password hashing algorithms (bcrypt, argon2) for passwords
Input entropy — Output security depends entirely on input entropy
Deterministic — Same input always yields the same output (no salt or randomness is added)
use alloy_provider::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::client::*;
use seismic_alloy_provider::precompiles;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://testnet-1.seismictest.net/rpc".parse()?;
let provider = SeismicProviderBuilder::new().connect_http(url);
// Using convenience helpers
let derived_key = precompiles::call::hkdf(&provider, b"my-input-key-material").await?;
println!("Derived key (convenience): 0x{}", hex::encode(&derived_key));
// Manual approach
let hkdf_address: Address =
"0x0000000000000000000000000000000000000068".parse()?;
// Derive key from input material
let ikm = b"my-input-key-material";
let input = Bytes::from(ikm.to_vec());
let result = provider
.call(
&TransactionRequest::default()
.to(hkdf_address)
.input(input.into()),
)
.await?;
let derived_key: [u8; 32] = result[..32].try_into()?;
println!("Derived key: 0x{}", hex::encode(derived_key));
Ok(())
}
use alloy_provider::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::client::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://testnet-1.seismictest.net/rpc".parse()?;
let provider = SeismicProviderBuilder::new().connect_http(url);
let hkdf_address: Address =
"0x0000000000000000000000000000000000000068".parse()?;
let master_secret = b"shared-master-key";
let contexts = [b"encryption" as &[u8], b"authentication", b"signing"];
for ctx in &contexts {
let mut ikm = master_secret.to_vec();
ikm.extend_from_slice(ctx);
let result = provider
.call(
&TransactionRequest::default()
.to(hkdf_address)
.input(Bytes::from(ikm).into()),
)
.await?;
let key: [u8; 32] = result[..32].try_into()?;
println!(
"Key for {}: 0x{}",
String::from_utf8_lossy(ctx),
hex::encode(key),
);
}
Ok(())
}
use alloy_provider::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::client::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://testnet-1.seismictest.net/rpc".parse()?;
let provider = SeismicProviderBuilder::new().connect_http(url);
// Step 1: Derive AES key via HKDF
let hkdf_address: Address =
"0x0000000000000000000000000000000000000068".parse()?;
let ikm = b"my-master-secret";
let hkdf_result = provider
.call(
&TransactionRequest::default()
.to(hkdf_address)
.input(Bytes::from(ikm.to_vec()).into()),
)
.await?;
let aes_key: [u8; 32] = hkdf_result[..32].try_into()?;
// Step 2: Use derived key for encryption
let encrypt_address: Address =
"0x0000000000000000000000000000000000000066".parse()?;
let nonce = [0u8; 12];
let plaintext = b"Encrypted with derived key";
let mut encrypt_input = Vec::new();
encrypt_input.extend_from_slice(&aes_key);
encrypt_input.extend_from_slice(&nonce);
encrypt_input.extend_from_slice(plaintext);
let ciphertext = provider
.call(
&TransactionRequest::default()
.to(encrypt_address)
.input(Bytes::from(encrypt_input).into()),
)
.await?;
println!("Ciphertext: 0x{}", hex::encode(&ciphertext));
Ok(())
}
use alloy_provider::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::client::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://testnet-1.seismictest.net/rpc".parse()?;
let provider = SeismicProviderBuilder::new().connect_http(url);
// Step 1: Perform ECDH
let ecdh_address: Address =
"0x0000000000000000000000000000000000000065".parse()?;
let my_sk: [u8; 32] = [/* ... */; 32];
let peer_pk: [u8; 33] = [/* ... */; 33];
let mut ecdh_input = Vec::with_capacity(65);
ecdh_input.extend_from_slice(&my_sk);
ecdh_input.extend_from_slice(&peer_pk);
let shared_secret = provider
.call(
&TransactionRequest::default()
.to(ecdh_address)
.input(Bytes::from(ecdh_input).into()),
)
.await?;
// Step 2: Further derive key from shared secret + application context
let hkdf_address: Address =
"0x0000000000000000000000000000000000000068".parse()?;
let mut ikm = shared_secret.to_vec();
ikm.extend_from_slice(b"application-context-v1");
let result = provider
.call(
&TransactionRequest::default()
.to(hkdf_address)
.input(Bytes::from(ikm).into()),
)
.await?;
let derived_key: [u8; 32] = result[..32].try_into()?;
println!("Derived key: 0x{}", hex::encode(derived_key));
Ok(())
}
use alloy_provider::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::client::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://testnet-1.seismictest.net/rpc".parse()?;
let provider = SeismicProviderBuilder::new().connect_http(url);
let hkdf_address: Address =
"0x0000000000000000000000000000000000000068".parse()?;
// Same input always produces the same output
let ikm = b"deterministic-input";
let input = Bytes::from(ikm.to_vec());
let key1 = provider
.call(
&TransactionRequest::default()
.to(hkdf_address)
.input(input.clone().into()),
)
.await?;
let key2 = provider
.call(
&TransactionRequest::default()
.to(hkdf_address)
.input(input.into()),
)
.await?;
assert_eq!(&key1[..32], &key2[..32]);
println!("Deterministic key: 0x{}", hex::encode(&key1[..32]));
Ok(())
}