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 theProver
as the first returned element (more on that here), which means thatProof
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 toonlyVerified
because it is automatically extracted frommsg.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>, } }
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 thejournalDigest
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 oneImageId
will fail to verify in the context of a differentImageId
.
Data encoding summary
Below, is a schema of how a block of data is encoded in different structures at different stages.
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
andCallAssumptions
structures are generated based on Solidity code from withsol!
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);
}
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 eachProver
contract, but only on development and test networks. This mode will be used if theProofMode
decoded fromSEAL
isFAKE
.PRODUCTION
- This requires infrastructure deployed ahead of time that performs actual verification. This mode will be used if theProofMode
decoded fromSEAL
isGROTH16
.