What the system actually proves

At a high level, the system proves:

“There exists a wallet address, a signature, and a challenge message such that:

  • They hash to these public commitments, and

  • The ECDSA signature is valid for that wallet and challenge.”

Critically, the verifier only ever sees Poseidon hashes and a Groth16 proof – never the raw wallet, signature, or UnifiedID.

Public vs private circuit inputs

The WalletOwnership.circom circuit is designed so that:

Public inputs (visible to any verifier, on- or off-chain):

  • walletAddressHash – Poseidon hash of the 20-byte wallet address

  • challengeHash – Poseidon hash of the padded 32-byte challenge message

Private inputs (known only to the prover / witness generator):

  • walletAddress[20] – raw Ethereum address bytes

  • signature[65] – ECDSA signature bytes (r || s || v)

  • challengeMessage[32] – padded challenge bytes (contains the UnifiedID text)

Outputs:

  • signatureHash – Poseidon hash of the 65-byte signature

  • ownershipProof – Poseidon hash of (walletHash, messageHash, signatureHash)

Circuit logic (WalletOwnership)

Conceptually:

template WalletOwnership() {
    signal input walletAddressHash;
    signal input challengeHash;

    signal input walletAddress[20];
    signal input signature[65];
    signal input challengeMessage[32];

    signal output signatureHash;
    signal output ownershipProof;

    // Hash the wallet bytes and bind them to the public commitment
    component walletHasher = HashBytes(20);
    // walletHasher.in[i] <== walletAddress[i];
    signal walletHash;
    walletHash <== walletHasher.out;
    walletHash === walletAddressHash;

    // Hash the challenge bytes and bind them to the public commitment
    component challengeHasher = HashBytes(32);
    // challengeHasher.in[i] <== challengeMessage[i];
    signal messageHash;
    messageHash <== challengeHasher.out;
    messageHash === challengeHash;

    // Hash the signature bytes
    component signatureHasher = HashBytes(65);
    // signatureHasher.in[i] <== signature[i];
    signatureHash <== signatureHasher.out;

    // Final ownership proof = Poseidon(walletHash, messageHash, signatureHash)
    component ownershipHasher = Poseidon(3);
    ownershipHasher.inputs[0] <== walletHash;
    ownershipHasher.inputs[1] <== messageHash;
    ownershipHasher.inputs[2] <== signatureHash;

    ownershipProof <== ownershipHasher.out;
}

Key point:

Verifiers see only walletAddressHash, challengeHash, signatureHash, ownershipProof and the Groth16 proof.

They never see:

  • walletAddress[20]

  • signature[65]

  • challengeMessage[32] (which contains the UnifiedID)

Last updated