Verify Proofs

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:

  1. 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;
}
  1. 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:

  1. 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)
  1. 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)
  1. Report Data: session_hash + script_hash (64 bytes)
  2. Digest: Hash of report data
  3. 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 in AttestationInput 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 attestation
  • nonce should include the 0x prefix if present in the original
  • sessionId must be the exact UUID string
  • version 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