mirror of
https://github.com/tlsnotary/tlsn.git
synced 2026-01-11 15:47:58 -05:00
Compare commits
3 Commits
plot_py
...
interactiv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bca4bf52ce | ||
|
|
0d1c8a4bd0 | ||
|
|
52b7bfc9d9 |
2185
Cargo.lock
generated
2185
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,8 @@ bincode = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
dotenv = { version = "0.15.0" }
|
||||
ethers = "2.0.14"
|
||||
eyre = "0.6.12"
|
||||
futures = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
@@ -26,6 +28,7 @@ hyper = { workspace = true, features = ["client", "http1"] }
|
||||
hyper-util = { workspace = true, features = ["full"] }
|
||||
k256 = { workspace = true, features = ["ecdsa"] }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
@@ -37,7 +40,12 @@ tokio = { workspace = true, features = [
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
rand.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "interactive"
|
||||
path = "interactive/interactive.rs"
|
||||
|
||||
[[example]]
|
||||
name = "eas"
|
||||
path = "eas/eas.rs"
|
||||
|
||||
36
crates/examples/eas/README.md
Normal file
36
crates/examples/eas/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## Simple Interactive Verifier + Ethereum Attestation Service
|
||||
|
||||
This example is basically the `interactive` example adding the generation of Offline [EAS](https://attest.org/) Attestation and timestamping it. Please first understand the interactive example and [EAS](https://attest.org/) before getting into this example.
|
||||
|
||||
This demo:
|
||||
|
||||
- Runs a simple interactive session between a Prover and a Verifier
|
||||
- Gets the redacted string of the response and generates an offline signed attestation of it. The attestation generated uses this [schema](https://sepolia.easscan.org/schema/view/0x938b5d03b0057688eef86d8101946311c4aaa740ffc39cef9bbfb6ce572a7198). It is stored in the `eas_attestation.json` local file.
|
||||
- Timestamps the attestation (that is mainly calling the `timestamp` method in the EAS contract), the Tx of the transaction will be shown.
|
||||
- You can verify the generated attestation with the provided `check-eas-attestation-js` example or using https://sepolia.easscan.org/tools.
|
||||
|
||||
To run this demo you need:
|
||||
|
||||
- Sepolia RPC provider URL
|
||||
- [Ethereum address secret key](https://support.metamask.io/configure/accounts/how-to-export-an-accounts-private-key/) with some ether.
|
||||
|
||||
This example fetches data from a local test server. To start this server, run:
|
||||
```shell
|
||||
PORT=4000 cargo run --bin tlsn-server-fixture
|
||||
```
|
||||
Next, run the interactive example with:
|
||||
```shell
|
||||
EAS_SK=<secret_key> RPC_URL=<rpc_url> SERVER_PORT=4000 cargo run --release --example eas
|
||||
```
|
||||
To view more detailed debug information, use the following command:
|
||||
```shell
|
||||
EAS_SK=<secret_key> RPC_URL=<rpc_url> RUST_LOG=debug,yamux=info,uid_mux=info SERVER_PORT=4000 cargo run --release --example eas
|
||||
```
|
||||
|
||||
This will generate a signed EAS attestation, you can verify it by using the eas-sdk with:
|
||||
```shell
|
||||
cd check-eas-attestation-js
|
||||
npm i
|
||||
RPC_URL=<rpc_url> npm run start
|
||||
```
|
||||
|
||||
1
crates/examples/eas/check-eas-attestation-js/.gitignore
vendored
Normal file
1
crates/examples/eas/check-eas-attestation-js/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
24
crates/examples/eas/check-eas-attestation-js/index.js
Normal file
24
crates/examples/eas/check-eas-attestation-js/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ethers } from "ethers";
|
||||
import { EAS, Offchain } from "@ethereum-attestation-service/eas-sdk";
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
async function main() {
|
||||
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
|
||||
const EAS_CONTRACT_ADDRESS = "0xC2679fBD37d54388Ce493F1DB75320D236e1815e";
|
||||
|
||||
const eas = new EAS(EAS_CONTRACT_ADDRESS);
|
||||
eas.connect(provider);
|
||||
|
||||
const serializedAttestation = readFileSync('../eas_attestation.json', 'utf8');
|
||||
const attestation = JSON.parse(serializedAttestation);
|
||||
|
||||
const offchain = await eas.getOffchain();
|
||||
try {
|
||||
const signatureOk = await offchain.verifyOffchainAttestationSignature(attestation.signer, attestation.sig);
|
||||
console.log("Signature verification:", signatureOk)
|
||||
} catch (err) {
|
||||
console.error("Invalid or malformed offchain attestation:", err);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
3491
crates/examples/eas/check-eas-attestation-js/package-lock.json
generated
Normal file
3491
crates/examples/eas/check-eas-attestation-js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
crates/examples/eas/check-eas-attestation-js/package.json
Normal file
14
crates/examples/eas/check-eas-attestation-js/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "check-eas-attestation",
|
||||
"version": "1.0.0",
|
||||
"description": "Verify EAS offline attestations with Node.js",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"ethers": "^6.0.0",
|
||||
"@ethereum-attestation-service/eas-sdk": "^1.0.0"
|
||||
}
|
||||
}
|
||||
1
crates/examples/eas/eas.abi
Normal file
1
crates/examples/eas/eas.abi
Normal file
File diff suppressed because one or more lines are too long
1
crates/examples/eas/eas.abi.url
Normal file
1
crates/examples/eas/eas.abi.url
Normal file
@@ -0,0 +1 @@
|
||||
https://sepolia.etherscan.io/address/0xC2679fBD37d54388Ce493F1DB75320D236e1815e#code
|
||||
579
crates/examples/eas/eas.rs
Normal file
579
crates/examples/eas/eas.rs
Normal file
@@ -0,0 +1,579 @@
|
||||
use std::{
|
||||
env,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use ethers::{
|
||||
abi::Address,
|
||||
signers::{LocalWallet, Signer},
|
||||
types::H256,
|
||||
};
|
||||
use http_body_util::Empty;
|
||||
use hyper::{body::Bytes, Request, StatusCode, Uri};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::instrument;
|
||||
|
||||
use tls_server_fixture::CA_CERT_DER;
|
||||
use tlsn::{
|
||||
config::{CertificateDer, ProtocolConfig, ProtocolConfigValidator, RootCertStore},
|
||||
connection::ServerName,
|
||||
prover::{ProveConfig, Prover, ProverConfig, TlsConfig},
|
||||
transcript::PartialTranscript,
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
};
|
||||
|
||||
use tlsn_server_fixture::DEFAULT_FIXTURE_PORT;
|
||||
use tlsn_server_fixture_certs::SERVER_DOMAIN;
|
||||
|
||||
const SECRET: &str = "random_auth_token";
|
||||
|
||||
// Maximum number of bytes that can be sent from prover to server.
|
||||
const MAX_SENT_DATA: usize = 1 << 12;
|
||||
// Maximum number of bytes that can be received by prover from server.
|
||||
const MAX_RECV_DATA: usize = 1 << 14;
|
||||
|
||||
// The contract address of the EAS contract on Sepolia.
|
||||
const EAS_ADDRESS: &str = "0xC2679fBD37d54388Ce493F1DB75320D236e1815e";
|
||||
// The chain ID of the Sepolia network.
|
||||
const EAS_CHAINID: u64 = 11155111;
|
||||
// The schema for the EAS attestation.
|
||||
const EAS_SCHEMA: &str = "0x938b5d03b0057688eef86d8101946311c4aaa740ffc39cef9bbfb6ce572a7198";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let rpc_url = env::var("RPC_URL").expect("RPC_URL environment variable must be set");
|
||||
let sk = env::var("EAS_SK").expect("EAS_SK environment variable must be set");
|
||||
let server_host: String = env::var("SERVER_HOST").unwrap_or("127.0.0.1".into());
|
||||
let server_port: u16 = env::var("SERVER_PORT")
|
||||
.map(|port| port.parse().expect("port should be valid integer"))
|
||||
.unwrap_or(DEFAULT_FIXTURE_PORT);
|
||||
|
||||
// We use SERVER_DOMAIN here to make sure it matches the domain in the test
|
||||
// server's certificate.
|
||||
let uri = format!("https://{SERVER_DOMAIN}:{server_port}/formats/html");
|
||||
let server_ip: IpAddr = server_host.parse().expect("Invalid IP address");
|
||||
let server_addr = SocketAddr::from((server_ip, server_port));
|
||||
|
||||
// Connect prover and verifier.
|
||||
let (prover_socket, verifier_socket) = tokio::io::duplex(1 << 23);
|
||||
let prover = prover(prover_socket, &server_addr, &uri);
|
||||
let verifier = verifier(verifier_socket);
|
||||
let (_, transcript) = tokio::join!(prover, verifier);
|
||||
|
||||
println!("Successfully verified {}", &uri);
|
||||
println!(
|
||||
"Verified sent data:\n{}",
|
||||
bytes_to_redacted_string(transcript.sent_unsafe())
|
||||
);
|
||||
println!(
|
||||
"Verified received data:\n{}",
|
||||
bytes_to_redacted_string(transcript.received_unsafe())
|
||||
);
|
||||
|
||||
// Generate EAS attestation
|
||||
|
||||
let signer = sk
|
||||
.parse::<LocalWallet>()
|
||||
.expect("Failed to parse LocalWallet")
|
||||
.with_chain_id(EAS_CHAINID);
|
||||
|
||||
println!("Using EAS signer: {}", signer.address());
|
||||
|
||||
let redacted_string = bytes_to_redacted_string(transcript.received_unsafe());
|
||||
let data = ethers::core::abi::encode(&[ethers::core::abi::Token::String(redacted_string)]);
|
||||
let verifying_contract = Address::from_str(EAS_ADDRESS).expect("Failed to parse EAS address");
|
||||
|
||||
let attestation = eas::OfflineAttestationBuilder {
|
||||
schema: H256::from_str(EAS_SCHEMA).unwrap(),
|
||||
recipient: Address::zero(),
|
||||
time: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
expiration_time: Some(0),
|
||||
ref_uid: None,
|
||||
revocable: true,
|
||||
nonce: Some(0),
|
||||
data,
|
||||
}
|
||||
.generate(&signer, verifying_contract)
|
||||
.await
|
||||
.expect("Failed to generate attestation");
|
||||
|
||||
std::fs::write(
|
||||
"eas_attestation.json",
|
||||
serde_json::to_string_pretty(&attestation).unwrap(),
|
||||
)
|
||||
.expect("Failed to write attestation to file");
|
||||
|
||||
println!("EAS Attestation generated and saved to eas_attestation.json");
|
||||
|
||||
// Timestamp the attestation using the EAS contract
|
||||
|
||||
_ = eas::timestamp_attestation(&signer, &rpc_url, &attestation)
|
||||
.await
|
||||
.expect("Failed to timestamp attestation");
|
||||
}
|
||||
|
||||
#[instrument(skip(verifier_socket))]
|
||||
async fn prover<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
verifier_socket: T,
|
||||
server_addr: &SocketAddr,
|
||||
uri: &str,
|
||||
) {
|
||||
let uri = uri.parse::<Uri>().unwrap();
|
||||
assert_eq!(uri.scheme().unwrap().as_str(), "https");
|
||||
let server_domain = uri.authority().unwrap().host();
|
||||
|
||||
// Create a root certificate store with the server-fixture's self-signed
|
||||
// certificate. This is only required for offline testing with the
|
||||
// server-fixture.
|
||||
let mut tls_config_builder = TlsConfig::builder();
|
||||
tls_config_builder.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
});
|
||||
let tls_config = tls_config_builder.build().unwrap();
|
||||
|
||||
// Set up protocol configuration for prover.
|
||||
let mut prover_config_builder = ProverConfig::builder();
|
||||
prover_config_builder
|
||||
.server_name(ServerName::Dns(server_domain.try_into().unwrap()))
|
||||
.tls_config(tls_config)
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let prover_config = prover_config_builder.build().unwrap();
|
||||
|
||||
// Create prover and connect to verifier.
|
||||
//
|
||||
// Perform the setup phase with the verifier.
|
||||
let prover = Prover::new(prover_config)
|
||||
.setup(verifier_socket.compat())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Connect to TLS Server.
|
||||
let tls_client_socket = tokio::net::TcpStream::connect(server_addr).await.unwrap();
|
||||
|
||||
// Pass server connection into the prover.
|
||||
let (mpc_tls_connection, prover_fut) =
|
||||
prover.connect(tls_client_socket.compat()).await.unwrap();
|
||||
|
||||
// Wrap the connection in a TokioIo compatibility layer to use it with hyper.
|
||||
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
|
||||
|
||||
// Spawn the Prover to run in the background.
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// MPC-TLS Handshake.
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Spawn the connection to run in the background.
|
||||
tokio::spawn(connection);
|
||||
|
||||
// MPC-TLS: Send Request and wait for Response.
|
||||
let request = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.header("Host", server_domain)
|
||||
.header("Connection", "close")
|
||||
.header("Secret", SECRET)
|
||||
.method("GET")
|
||||
.body(Empty::<Bytes>::new())
|
||||
.unwrap();
|
||||
let response = request_sender.send_request(request).await.unwrap();
|
||||
|
||||
assert!(response.status() == StatusCode::OK);
|
||||
|
||||
// Create proof for the Verifier.
|
||||
let mut prover = prover_task.await.unwrap().unwrap();
|
||||
|
||||
let mut builder = ProveConfig::builder(prover.transcript());
|
||||
|
||||
// Reveal the DNS name.
|
||||
builder.server_identity();
|
||||
|
||||
// Find the secret in the request.
|
||||
let pos = prover
|
||||
.transcript()
|
||||
.sent()
|
||||
.windows(SECRET.len())
|
||||
.position(|w| w == SECRET.as_bytes())
|
||||
.expect("the secret should be in the sent data");
|
||||
|
||||
// Reveal everything except for the secret.
|
||||
builder.reveal_sent(&(0..pos)).unwrap();
|
||||
builder
|
||||
.reveal_sent(&(pos + SECRET.len()..prover.transcript().sent().len()))
|
||||
.unwrap();
|
||||
|
||||
// Find the substring "Dick".
|
||||
let pos = prover
|
||||
.transcript()
|
||||
.received()
|
||||
.windows(4)
|
||||
.position(|w| w == b"Dick")
|
||||
.expect("the substring 'Dick' should be in the received data");
|
||||
|
||||
// Reveal everything except for the substring.
|
||||
builder.reveal_recv(&(0..pos)).unwrap();
|
||||
builder
|
||||
.reveal_recv(&(pos + 4..prover.transcript().received().len()))
|
||||
.unwrap();
|
||||
|
||||
let config = builder.build().unwrap();
|
||||
|
||||
prover.prove(&config).await.unwrap();
|
||||
prover.close().await.unwrap();
|
||||
}
|
||||
|
||||
#[instrument(skip(socket))]
|
||||
async fn verifier<T: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
|
||||
socket: T,
|
||||
) -> PartialTranscript {
|
||||
// Set up Verifier.
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(MAX_SENT_DATA)
|
||||
.max_recv_data(MAX_RECV_DATA)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Create a root certificate store with the server-fixture's self-signed
|
||||
// certificate. This is only required for offline testing with the
|
||||
// server-fixture.
|
||||
let verifier_config = VerifierConfig::builder()
|
||||
.root_store(RootCertStore {
|
||||
roots: vec![CertificateDer(CA_CERT_DER.to_vec())],
|
||||
})
|
||||
.protocol_config_validator(config_validator)
|
||||
.build()
|
||||
.unwrap();
|
||||
let verifier = Verifier::new(verifier_config);
|
||||
|
||||
// Receive authenticated data.
|
||||
let VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
..
|
||||
} = verifier
|
||||
.verify(socket.compat(), &VerifyConfig::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let server_name = server_name.expect("prover should have revealed server name");
|
||||
let transcript = transcript.expect("prover should have revealed transcript data");
|
||||
|
||||
// Check sent data.
|
||||
let sent = transcript.sent_unsafe().to_vec();
|
||||
let sent_data = String::from_utf8(sent.clone()).expect("Verifier expected sent data");
|
||||
sent_data
|
||||
.find(SERVER_DOMAIN)
|
||||
.unwrap_or_else(|| panic!("Verification failed: Expected host {SERVER_DOMAIN}"));
|
||||
|
||||
// Check received data.
|
||||
let received = transcript.received_unsafe().to_vec();
|
||||
let response = String::from_utf8(received.clone()).expect("Verifier expected received data");
|
||||
response
|
||||
.find("Herman Melville")
|
||||
.unwrap_or_else(|| panic!("Expected valid data from {SERVER_DOMAIN}"));
|
||||
|
||||
// Check Session info: server name.
|
||||
let ServerName::Dns(server_name) = server_name;
|
||||
assert_eq!(server_name.as_str(), SERVER_DOMAIN);
|
||||
|
||||
transcript
|
||||
}
|
||||
|
||||
/// Render redacted bytes as `🙈`.
|
||||
fn bytes_to_redacted_string(bytes: &[u8]) -> String {
|
||||
String::from_utf8(bytes.to_vec())
|
||||
.unwrap()
|
||||
.replace('\0', "🙈")
|
||||
}
|
||||
|
||||
mod eas {
|
||||
|
||||
use std::{collections::BTreeMap, str::FromStr};
|
||||
|
||||
use super::*;
|
||||
use ethers::{
|
||||
abi::{Abi, Token},
|
||||
middleware::{
|
||||
gas_escalator::{Frequency, GeometricGasPrice},
|
||||
GasEscalatorMiddleware, MiddlewareBuilder, NonceManagerMiddleware, SignerMiddleware,
|
||||
},
|
||||
providers::{Http, Provider},
|
||||
signers::{Signer, Wallet},
|
||||
types::{
|
||||
transaction::eip712::{EIP712Domain, Eip712DomainType, TypedData},
|
||||
Address, Signature, H256, U256,
|
||||
},
|
||||
utils::{hex, keccak256, to_checksum},
|
||||
};
|
||||
use k256::ecdsa::SigningKey;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
|
||||
// A signed offline attestation, which includes the signature and the signer's
|
||||
// address.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SignedOfflineAttestation {
|
||||
/// The attestation and its signature.
|
||||
pub sig: Sig,
|
||||
/// The address of the signer.
|
||||
pub signer: Address,
|
||||
}
|
||||
|
||||
/// Represents the domain for EIP-712 typed data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Domain {
|
||||
/// The name of the domain.
|
||||
pub name: String,
|
||||
/// The version of the domain.
|
||||
pub version: String,
|
||||
/// The chain ID of the domain.
|
||||
#[serde(rename = "chainId")]
|
||||
pub chain_id: String,
|
||||
/// The address of the verifying contract.
|
||||
#[serde(rename = "verifyingContract")]
|
||||
pub verifying_contract: String,
|
||||
}
|
||||
|
||||
/// The Signed data and its signature for an offline attestation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Sig {
|
||||
/// The version of the attestation.
|
||||
pub version: u16,
|
||||
/// The uid of the attestation, which is a hash of the attestation data.
|
||||
pub uid: String,
|
||||
|
||||
/// The domain of the attestation.
|
||||
pub domain: Domain,
|
||||
#[serde(rename = "primaryType")]
|
||||
/// The primary type of the attestation.
|
||||
pub primary_type: String,
|
||||
/// The types that the attestation manages, which is a map of type names
|
||||
/// to their definitions.
|
||||
pub types: BTreeMap<String, Vec<Eip712DomainType>>,
|
||||
/// The message of the attestation, which is a map of field names to
|
||||
/// their values.
|
||||
pub message: BTreeMap<String, Value>,
|
||||
|
||||
/// The secp256k1 signature of the attestation.
|
||||
pub signature: Signature,
|
||||
}
|
||||
|
||||
/// Information to build an offline attestation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OfflineAttestationBuilder {
|
||||
pub schema: H256,
|
||||
pub recipient: Address,
|
||||
pub time: u64,
|
||||
pub expiration_time: Option<u64>,
|
||||
pub ref_uid: Option<H256>,
|
||||
pub revocable: bool,
|
||||
pub nonce: Option<u64>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl OfflineAttestationBuilder {
|
||||
/// Create and sign and offline attestation, returning a
|
||||
/// `SignedOfflineAttestation`.
|
||||
pub async fn generate<S: Signer>(
|
||||
&self,
|
||||
signer: &S,
|
||||
verifying_contract: Address,
|
||||
) -> Result<SignedOfflineAttestation, <S as Signer>::Error> {
|
||||
let expiration_time = self.expiration_time.unwrap_or(0);
|
||||
|
||||
let mut salt = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut salt);
|
||||
|
||||
// the domain for the EIP-712 typed data.
|
||||
|
||||
let domain = EIP712Domain {
|
||||
name: Some("EAS Attestation".to_string()),
|
||||
version: Some("0.26".to_string()),
|
||||
chain_id: Some(U256::from(signer.chain_id())),
|
||||
verifying_contract: Some(verifying_contract),
|
||||
salt: None,
|
||||
};
|
||||
|
||||
// the typed data for the EIP-712 signature.
|
||||
|
||||
let typed_data = TypedData {
|
||||
domain,
|
||||
primary_type: "Attest".to_string(),
|
||||
types: BTreeMap::from([(
|
||||
"Attest".to_string(),
|
||||
vec![
|
||||
domtype("version", "uint16"),
|
||||
domtype("schema", "bytes32"),
|
||||
domtype("recipient", "address"),
|
||||
domtype("time", "uint64"),
|
||||
domtype("expirationTime", "uint64"),
|
||||
domtype("revocable", "bool"),
|
||||
domtype("refUID", "bytes32"),
|
||||
domtype("data", "bytes"),
|
||||
domtype("salt", "bytes32"),
|
||||
],
|
||||
)]),
|
||||
message: BTreeMap::from([
|
||||
("version".to_string(), Value::Number(Number::from(2))),
|
||||
(
|
||||
"schema".to_string(),
|
||||
Value::String(format!("0x{}", hex::encode(self.schema))),
|
||||
),
|
||||
(
|
||||
"recipient".to_string(),
|
||||
Value::String(format!("{:#x}", self.recipient)),
|
||||
),
|
||||
("time".to_string(), Value::String(self.time.to_string())),
|
||||
(
|
||||
"expirationTime".to_string(),
|
||||
Value::String(expiration_time.to_string()),
|
||||
),
|
||||
(
|
||||
"refUID".to_string(),
|
||||
Value::String(format!(
|
||||
"0x{}",
|
||||
hex::encode(self.ref_uid.unwrap_or_default())
|
||||
)),
|
||||
),
|
||||
("revocable".to_string(), Value::Bool(self.revocable)),
|
||||
(
|
||||
"data".to_string(),
|
||||
Value::String(format!("0x{}", hex::encode(&self.data))),
|
||||
),
|
||||
(
|
||||
"nonce".to_string(),
|
||||
Value::Number(Number::from(self.nonce.unwrap_or(0))),
|
||||
),
|
||||
(
|
||||
"salt".to_string(),
|
||||
Value::String(format!("0x{}", hex::encode(salt))),
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
// the unique identifier (uid) for the attestation, which is a hash of the ABI
|
||||
// encoded data.
|
||||
|
||||
let uid = {
|
||||
let tokens = vec![
|
||||
Token::FixedBytes(2u16.to_be_bytes().to_vec()),
|
||||
Token::Bytes(format!("0x{}", hex::encode(self.schema)).into_bytes()),
|
||||
Token::Address(self.recipient),
|
||||
Token::Address(Address::zero()),
|
||||
Token::FixedBytes(self.time.to_be_bytes().to_vec()),
|
||||
Token::FixedBytes(expiration_time.to_be_bytes().to_vec()),
|
||||
Token::Bool(self.revocable),
|
||||
Token::FixedBytes(self.ref_uid.unwrap_or_default().0.to_vec()),
|
||||
Token::Bytes(self.data.clone()),
|
||||
Token::FixedBytes(salt.to_vec()),
|
||||
Token::FixedBytes(0u32.to_be_bytes().to_vec()),
|
||||
];
|
||||
|
||||
let encoded = ethers::core::abi::encode_packed(&tokens).unwrap();
|
||||
keccak256(encoded)
|
||||
};
|
||||
|
||||
// Sign the typed data using the signer's private key.
|
||||
let signature = signer.sign_typed_data(&typed_data).await?;
|
||||
|
||||
// generate the signed offline attestation.
|
||||
let offline_attestation = SignedOfflineAttestation {
|
||||
sig: Sig {
|
||||
version: 2,
|
||||
uid: format!("0x{}", hex::encode(uid)),
|
||||
domain: Domain {
|
||||
name: typed_data.domain.name.unwrap(),
|
||||
version: typed_data.domain.version.unwrap().to_string(),
|
||||
chain_id: typed_data.domain.chain_id.unwrap().to_string(),
|
||||
verifying_contract: to_checksum(
|
||||
&typed_data.domain.verifying_contract.unwrap(),
|
||||
None,
|
||||
),
|
||||
},
|
||||
primary_type: typed_data.primary_type,
|
||||
types: typed_data.r#types,
|
||||
message: typed_data.message,
|
||||
signature,
|
||||
},
|
||||
signer: signer.address(),
|
||||
};
|
||||
|
||||
Ok(offline_attestation)
|
||||
}
|
||||
}
|
||||
|
||||
fn domtype(name: &str, type_name: &str) -> Eip712DomainType {
|
||||
Eip712DomainType {
|
||||
name: name.to_string(),
|
||||
r#type: type_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Timestamps an offline attestation by sending a transaction to the EAS
|
||||
/// contract.
|
||||
pub async fn timestamp_attestation(
|
||||
signer: &Wallet<SigningKey>,
|
||||
rpc_url: &str,
|
||||
attestation: &SignedOfflineAttestation,
|
||||
) -> Result<H256, eyre::ErrReport> {
|
||||
// Set up signer
|
||||
|
||||
let signer_address = signer.address();
|
||||
let escalator = GeometricGasPrice::new(1.125, 60_u64, None::<u64>);
|
||||
let provider = Provider::<Http>::try_from(rpc_url)?
|
||||
.wrap_into(|p| GasEscalatorMiddleware::new(p, escalator, Frequency::PerBlock))
|
||||
.wrap_into(|p| SignerMiddleware::new(p, signer.clone()))
|
||||
.wrap_into(|p| NonceManagerMiddleware::new(p, signer_address)); // Outermost layer
|
||||
|
||||
// Read ABI and create contract instance
|
||||
|
||||
let abi: Abi = serde_json::from_str(include_str!("eas.abi"))?;
|
||||
let contract = ethers::contract::Contract::new(
|
||||
EAS_ADDRESS.parse::<Address>().unwrap(),
|
||||
abi,
|
||||
provider.into(),
|
||||
);
|
||||
|
||||
// Call Timestamp(bytes32 uid) method
|
||||
|
||||
let uid =
|
||||
H256::from_str(&attestation.sig.uid).map_err(|_| eyre::eyre!("Invalid UID format"))?;
|
||||
|
||||
let method = contract.method::<_, H256>("timestamp", uid)?;
|
||||
let pending_tx = method.send().await?;
|
||||
|
||||
// Wait for transaction confirmation
|
||||
|
||||
println!("EAS Timestamping attestation: {:?}", pending_tx);
|
||||
|
||||
let receipt = pending_tx
|
||||
.confirmations(1)
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("Transaction receipt not found"))?;
|
||||
|
||||
println!(
|
||||
"EAS Attestation timestamped: {:?}",
|
||||
receipt.transaction_hash
|
||||
);
|
||||
|
||||
Ok(receipt.transaction_hash)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user