Verify Proofs
Pluto’s attestation system creates cryptographic proofs that a specific script was executed at a specific time with specific data. The signature verification process ensures the integrity of both the execution environment (script) and the execution context (session). This guide shows how to interpret and verify Pluto notary signatures.
Signature verification testnet contract is deployed on Base Sepolia at 0xeB3Ba25400A4d716d091090942705aD51d04B7ea.
Try it out
Contract Function Signature
Contract can be found at the solidity-verifier repo. Please reach out to us, if you encounter or discover any issue in the verification.
function verifyAttestation(
AttestationInput memory input,
AttestationSignature memory signature
) public returns (bool)
Where the structs are defined as:
struct AttestationInput {
string version; // Protocol version (e.g., "v1")
string scriptRaw; // The raw script content that was executed
string issuedAt; // RFC3339 timestamp when attestation was created
string nonce; // Random nonce for replay protection
string sessionId; // UUID identifying the execution session
ProofData[] data; // Key-value pairs of proof data
}
struct AttestationSignature {
bytes32 digest; // Hash of the attestation data
uint8 v; // Recovery parameter (27 or 28)
bytes32 r; // First 32 bytes of signature
bytes32 s; // Second 32 bytes of signature
address expectedSigner; // Expected signer address (must be a registered notary)
}
struct ProofData {
string key; // The key for the proof data
string value; // The value for the proof data
}
Verification Logic
The contract performs these critical checks:
- Calculate and Verify Hashes
// Calculate script hash
bytes32 scriptHash = calculateScriptHash(input.version, input.scriptRaw);
// Calculate session hash
bytes32 sessionHash = calculateSessionHash(
input.version, input.issuedAt, input.nonce, input.sessionId, input.data
);
// Calculate digest
bytes32 digest = calculateDigest(sessionHash, scriptHash);
// Verify the digest matches
if (digest != signature.digest) {
return false;
}
- Verify Signature via Verifier Contract
The verifyAttestation
function calls the underlying Verifier
contract which performs:
- Notary Check: Verifies the signer is a registered notary
- Digest Verification: Ensures digest = keccak256(sessionHash || scriptHash)
- Signature Recovery: Uses ecrecover to verify the signature
- Replay Protection: Prevents duplicate submissions per wallet
bool success = verifier.verifyNotarySignature(
signature.digest, signature.v, signature.r, signature.s,
signature.expectedSigner, scriptHash, sessionHash
);
Step-by-Step Verification
Step 1: Extract Data from Pluto Attestation
From the Pluto attestation JSON, you need these fields:
{
"version": "v1",
"script": {
"raw": "import { createSession } from '@plutoxyz/automation';\n..."
},
"issued_at": "2025-07-03T15:03:57Z",
"nonce": "0x3935659ffb235e15",
"session_id": "8bc45a6a-7b61-4828-a513-f6913535547a",
"data": {
"bank_balance": "8910.3"
},
"digest": "0x2843da5e7e228b273bb71b3a020dbc62799b5b4c032de361a4783f57f1223809",
"signature_r": "0x53997b7d7a63a6e86c37f8d31a2a049303678afe69672e74368b6221ab56ee81",
"signature_s": "0x5e7c985d30fde9589ad38617f09cac0814a4b385c5436b6805896d33dc49efaf",
"signature_v": 28,
"signer": "0x209Af77DfDaba352890b0Bc9B86A25bE67eF436A"
}
Step 2: Understand the Data Flow
The attestation contains pre-computed hashes, but understanding their creation helps with verification:
- Script Hash: Hash of version + script content
- Version: Protocol version string (like “v1”) - ensures compatibility across protocol upgrades
- Script Raw Content: The actual executable code/script that was run
script_hash = Keccak256(version + script)
- Session Hash: Hash of version + timestamp + nonce + session ID + proof data
- Version: Same protocol version as script hash
- IssuedAt: RFC3339 formatted timestamp of when the attestation was created
- Nonce: 8 random bytes (hex encoded with 0x prefix) to prevent replay attacks
- SessionId: UUID string identifying this specific execution session
- Data: Key-value pairs of proof data, sorted by key for deterministic hashing
session_hash = Keccak256(version + issued_at + nonce + session_id + prove_data)
- Report Data: session_hash + script_hash (64 bytes)
- Digest: Hash of report data
- Signature: ECDSA signature of digest
Step 3: Prepare Function Arguments
Map the attestation fields to the struct parameters:
AttestationInput struct:
version: = attestation.version
scriptRaw: = attestation.script.raw
issuedAt: = attestation.issued_at
nonce: = attestation.nonce
sessionId: = attestation.session_id
data: = [{key: "bank_balance", value: "8910.3"}] // from attestation.data
AttestationSignature struct:
digest: = attestation.digest
v: = attestation.signature_v
r: = attestation.signature_r
s: = attestation.signature_s
expectedSigner: = attestation.signer
Step 4: Format for Transaction
Ensure proof data is sorted by key alphabetically and properly formatted:
const attestationInput = {
version: "v1",
scriptRaw: "import { createSession } from '@plutoxyz/automation';\n...",
issuedAt: "2025-07-03T15:03:57Z",
nonce: "0x3935659ffb235e15",
sessionId: "8bc45a6a-7b61-4828-a513-f6913535547a",
data: [{ key: "bank_balance", value: "8910.3" }],
};
const attestationSignature = {
digest: "0x2843da5e7e228b273bb71b3a020dbc62799b5b4c032de361a4783f57f1223809",
v: 28,
r: "0x53997b7d7a63a6e86c37f8d31a2a049303678afe69672e74368b6221ab56ee81",
s: "0x5e7c985d30fde9589ad38617f09cac0814a4b385c5436b6805896d33dc49efaf",
expectedSigner: "0x209Af77DfDaba352890b0Bc9B86A25bE67eF436A",
};
Common Mistakes to Avoid
Click to expand
Incorrect Struct Assembly
The most common mistake is incorrectly assembling the AttestationInput
and AttestationSignature
structs:
- Ensure all string fields are exactly as they appear in the original attestation
- Proof data must be sorted alphabetically by key for deterministic hashing
- Don’t modify script content, timestamps, or other fields
Proof Data Ordering
- The
data
array inAttestationInput
must have keys sorted alphabetically - This ensures deterministic hash calculation across different implementations
- Original attestation order may differ from sorted order required for verification
String Field Formatting
issuedAt
must be in RFC3339 format exactly as in the attestationnonce
should include the0x
prefix if present in the originalsessionId
must be the exact UUID stringversion
should match exactly (typically “v1”)
Script Content Issues
- The
scriptRaw
field must contain the exact script content including newlines - Don’t modify or reformat the script code
- Preserve all whitespace and formatting from the original attestation
V Parameter Confusion
- Pluto uses Ethereum’s recovery parameter convention (27 or 28)
- Your example shows
signature_v: 28
- pass this value directly - Don’t subtract 27 - pass the value as-is
Notary Validation
- The underlying verifier contract only accepts signatures from registered notaries
- Ensure the
expectedSigner
address is a trusted Pluto notary - Invalid notary addresses will cause the transaction to revert
Duplicate Proof Submission
- The contract prevents replay attacks by tracking used proofs per wallet
- Each proof can only be submitted once by the same wallet address
- Different wallets CAN submit the same proof (only same wallet + same proof is blocked)
- Attempting to resubmit from the same wallet will cause
DuplicateProof()
revert
Hash Recreation Errors
If you’re recreating hashes for verification:
- Use Keccak256 (not SHA3-256) - they’re different algorithms
- Maintain exact byte order when concatenating
- Sort proof data keys alphabetically
- Use exact string formatting (RFC3339 for timestamps, hex with 0x for nonces)
Endianness Issues
- All values are big-endian (most significant byte first)
- Don’t reverse byte order when converting between formats