Decrypt data on-chain using Mercury EVM's AES-GCM decryption precompile.
Overview
The AES-GCM decryption precompile at address 0x67 performs AES-256-GCM authenticated decryption. The ciphertext must include the 16-byte authentication tag appended during encryption. If the tag does not verify, the precompile reverts.
Precompile Address
0x0000000000000000000000000000000000000067
Input Encoding
Field
Size
Description
key
32 bytes
AES-256 decryption key (must match encryption key)
nonce
12 bytes
Nonce (must match the nonce used during encryption)
ciphertext
Variable
Encrypted data including the 16-byte authentication tag
The input is the concatenation of key (32 bytes) + nonce (12 bytes) + ciphertext (variable length, includes 16-byte tag).
use alloy::providers::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::foundry::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://gcp-1.seismictest.net/rpc".parse()?;
let provider = sreth_unsigned_provider(url);
let encrypt_address: Address =
"0x0000000000000000000000000000000000000066".parse()?;
let decrypt_address: Address =
"0x0000000000000000000000000000000000000067".parse()?;
let key = [0x42u8; 32];
let nonce = [0u8; 12];
let plaintext = b"Secret message";
// Encrypt first
let mut encrypt_input = Vec::new();
encrypt_input.extend_from_slice(&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?;
// Decrypt
let mut decrypt_input = Vec::new();
decrypt_input.extend_from_slice(&key);
decrypt_input.extend_from_slice(&nonce);
decrypt_input.extend_from_slice(&ciphertext);
let decrypted = provider
.call(
&TransactionRequest::default()
.to(decrypt_address)
.input(Bytes::from(decrypt_input).into()),
)
.await?;
assert_eq!(&decrypted[..], plaintext);
println!("Decrypted: {}", String::from_utf8_lossy(&decrypted));
Ok(())
}
use alloy::providers::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::foundry::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://gcp-1.seismictest.net/rpc".parse()?;
let provider = sreth_unsigned_provider(url);
let encrypt_address: Address =
"0x0000000000000000000000000000000000000066".parse()?;
let decrypt_address: Address =
"0x0000000000000000000000000000000000000067".parse()?;
let key = [0x42u8; 32];
let messages = ["Message 1", "Message 2", "Message 3"];
let mut ciphertexts = Vec::new();
// Encrypt each message with a unique nonce
for (i, msg) in messages.iter().enumerate() {
let mut nonce = [0u8; 12];
nonce[11] = i as u8;
let mut input = Vec::new();
input.extend_from_slice(&key);
input.extend_from_slice(&nonce);
input.extend_from_slice(msg.as_bytes());
let ct = provider
.call(
&TransactionRequest::default()
.to(encrypt_address)
.input(Bytes::from(input).into()),
)
.await?;
ciphertexts.push((nonce, ct));
}
// Decrypt all messages
for (i, (nonce, ct)) in ciphertexts.iter().enumerate() {
let mut input = Vec::new();
input.extend_from_slice(&key);
input.extend_from_slice(nonce);
input.extend_from_slice(ct);
let decrypted = provider
.call(
&TransactionRequest::default()
.to(decrypt_address)
.input(Bytes::from(input).into()),
)
.await?;
println!("Message {i}: {}", String::from_utf8_lossy(&decrypted));
}
Ok(())
}
use alloy::providers::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::foundry::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://gcp-1.seismictest.net/rpc".parse()?;
let provider = sreth_unsigned_provider(url);
let decrypt_address: Address =
"0x0000000000000000000000000000000000000067".parse()?;
let key = [0x42u8; 32];
let nonce = [0u8; 12];
let bad_ciphertext = b"this is not valid ciphertext";
let mut input = Vec::new();
input.extend_from_slice(&key);
input.extend_from_slice(&nonce);
input.extend_from_slice(bad_ciphertext);
// Decryption will fail if the authentication tag doesn't verify
match provider
.call(
&TransactionRequest::default()
.to(decrypt_address)
.input(Bytes::from(input).into()),
)
.await
{
Ok(plaintext) => println!("Decrypted: 0x{}", hex::encode(&plaintext)),
Err(e) => println!("Decryption failed (expected): {e}"),
}
Ok(())
}
use alloy::providers::Provider;
use alloy_primitives::{Address, Bytes};
use alloy_rpc_types_eth::TransactionRequest;
use seismic_prelude::foundry::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://gcp-1.seismictest.net/rpc".parse()?;
let provider = sreth_unsigned_provider(url);
// Bob derives the same shared secret that Alice used to encrypt
let ecdh_address: Address =
"0x0000000000000000000000000000000000000065".parse()?;
let bob_sk: [u8; 32] = [/* Bob's private key */; 32];
let alice_pk: [u8; 33] = [/* Alice's compressed public key */; 33];
let mut ecdh_input = Vec::with_capacity(65);
ecdh_input.extend_from_slice(&bob_sk);
ecdh_input.extend_from_slice(&alice_pk);
let ecdh_result = provider
.call(
&TransactionRequest::default()
.to(ecdh_address)
.input(Bytes::from(ecdh_input).into()),
)
.await?;
let aes_key: [u8; 32] = ecdh_result[..32].try_into()?;
// Decrypt the ciphertext Alice sent
let decrypt_address: Address =
"0x0000000000000000000000000000000000000067".parse()?;
let nonce = [0u8; 12]; // Must match Alice's encryption nonce
let ciphertext: &[u8] = &[/* ciphertext from Alice */];
let mut decrypt_input = Vec::new();
decrypt_input.extend_from_slice(&aes_key);
decrypt_input.extend_from_slice(&nonce);
decrypt_input.extend_from_slice(ciphertext);
let decrypted = provider
.call(
&TransactionRequest::default()
.to(decrypt_address)
.input(Bytes::from(decrypt_input).into()),
)
.await?;
println!("Bob received: {}", String::from_utf8_lossy(&decrypted));
Ok(())
}