Solidity

Proving

On-chain verification is implemented by using a customized verification function. It receives a list of arguments in the same order as returned by the Prover (public output).

Proof structure must always be returned from the Prover as the first returned element (more on that here), which means that Proof structure must also be passed as the first argument to the verification function.

The verification function should use the onlyVerified() modifier, which takes two arguments: the address of a smart contract and a selector of function that was executed in the Prover contract.

See an example verification function below:

contract Example is Verifier {

    function claim(Proof _p, address verifiedArg1, uint verifiedArg2, bytes extraArg) public returns (uint)
        onlyVerified(PROVER_ADDRESS, FUNCTION_SELECTOR) {
        //...
    }

}

proof is not an argument to onlyVerified because it is automatically extracted from msg.data.

Data flow

Proving data flow consists of three steps:

Step 1: GuestOutput

It starts at Guest, which returns GuestOutput structure. GuestOutput consists of just one field - evm_call_result. evm_call_result field is abi encoded Prover function output. Since Prover returns Proof placeholder as its first returned value, Guest pre-fills length and call_assumptions fields of the Proof structuture.

length field of Proof structure is equal to the length of abi encoded public outputs, not including the size of the Proof placeholder.

See the code snippets below for pseudocode:

#![allow(unused)]
fn main() {
pub struct GuestOutput {
    pub evm_call_result: Vec<u8>,
}
}

Schema

Step 2: Host output as v_call result

In the next step, the Host replaces the seal field in the Proof placeholder with the actual seal, which is a cryptographic proof of the Prover's execution. The Host then returns this via the JSON-RPC v_call method, delivering the seal as a byte string in the result field.

This approach allows the smart contract developer to decode the v_call result as though they were decoding the Prover function's output directly. In other words, the v_call result is compatible with, and can be decoded according to, the ABI of the called Prover function.

In this step, the Host also fills in the field callGuestId, which is a hint to the Verifier about the version of the Guest program.

Step 3: Verifier call

Finally, the method on the on-chain smart contract is called to verify the proof. More on that in the next section.

Proof verification

To verify a zero-knowledge proof, vlayer uses a verify function, delivered by Risc-0.

function verify(Seal calldata seal, bytes32 imageId, bytes32 journalDigest) { /* ... */ }

onlyVerified gets seal and journalDigest by slicing it out of msg.data.

length field of Proof structure is used, when guest output bytes are restored in Solidity in order to compute journalDigest. length field hints the verifier, which bytes should be included in the journal, since they belong to encoding of the public outputs, and which bytes belong to extra arguments, passed additionally in calldata.

imageId is fixed on blockchain and updated on each new version of vlayer.

ImageId

The ImageId is an indicator of the specific Guest program used to generate a proof. A simple mental model is that the ImageId is a digest of the ELF file executed within the zkvm and of its boot environment. More information on ImageId can be found here.

The ImageId can change frequently, especially on testnets, since any update to the Guest code changes the executable bytecode, which in turn changes the ImageId. This is a desirable feature because it assures developers that the exact Guest code executed is the one they expected. It also prevents attackers from providing a proof generated by a malicious or incorrect Guest that would falsely attest to a particular state.

The guestCallId returned by the vlayer prover improves error handling and enables the whitelisting of specific ImageIds.

Note: The callGuestId field is not part of the journalDigest and therefore is not cryptographically validated meaning transaction sender can try to put overwrite this field. However, this does not impact security, since proofs generated for one ImageId will fail to verify in the context of a different ImageId.

Data encoding summary

Below, is a schema of how a block of data is encoded in different structures at different stages.

Schema

Structures

The Proof structure looks as follows:

struct Proof {
    uint32 length;
    Seal seal;
    CallAssumptions callAssumptions;
}

with Seal having the following structure:

enum ProofMode {
    GROTH16,
    FAKE
}

struct Seal {
    bytes32[8] seal;
    ProofMode mode;
}

and the following structure of CallAssumptions:

struct CallAssumptions {
    address proverContractAddress;
    bytes4 functionSelector;
    uint256 settleBlockNumber;
    bytes32 settleBlockHash;
}

Note that Proof, Seal and CallAssumptions structures are generated based on Solidity code from with sol! macro.

Feature-specific

Libraries

library EmailProofLib {
    function verify(UnverifiedEmail memory unverifiedEmail) internal view returns (VerifiedEmail memory);
}

library WebProofLib {
    function verify(WebProof memory webProof, string memory dataUrl) internal view returns (Web memory);
    function recover(WebProof memory webProof) internal view returns (Web memory);
}
Note that `EmailProofLib.verify()` and `WebProofLib.verify()` functions are intended to be called during the proving process, unlike the `Verifier.verify()`.

Structures

Unverified Email

The UnverifiedEmail is passed into the EmailProofLib.verify() function. It returns the VerifiedEmail struct, described below.

struct UnverifiedEmail {
    string email; // Raw MIME-encoded email
    DnsRecord dnsRecord;
    VerificationData verificationData;
}

// Describes DNS record, according to DoH spec
struct DnsRecord {
    string name;
    uint8 recordType;
    string data;
    uint64 ttl;
}

// Signature data of the DNS record
struct VerificationData {
    uint64 validUntil; // Signature expiration timestamp
    bytes signature; // DNS Notary signature of the serialized DNS record
    bytes pubKey; // Public key used for signature
}

Verified Email

struct VerifiedEmail {
    string from; // Sender email address
    string to; // Recipient email address
    string subject; // Email subject
    string body; // Email body
}

Two Proving Modes

To support two proving modes, vlayer provides a set of smart contracts connected to the Verifier contract, one for each mode:

  • DEVELOPMENT - Automatically deployed with each Prover contract, but only on development and test networks. This mode will be used if the ProofMode decoded from SEAL is FAKE.
  • PRODUCTION - This requires infrastructure deployed ahead of time that performs actual verification. This mode will be used if the ProofMode decoded from SEAL is GROTH16.