Solidity skeleton
Here is an example of how a Solidity smart contract can digest and use a proof from a Cairo program.
This is a short example of a system that involves a smart contract interacting with a Cairo program. The example highlights how Cairo programs and their proofs can interact with the Ethereum blockchain.
The user provides a special number to a Cairo program. A proof is generated and integrated into the Ethereum blockchain. A smart contract checks the proof and then accepts the special number and performs an action using that number.
Set up program inputs
Create an input.json
file in the same directory as the cairo code with the following contents.
{
"current_number": 300,
"special_number": 556
}
Set up Cairo program
Create a file called program.cairo
with the following contents:
%builtins output
from starkware.cairo.common.serialize import serialize_word
func main{output_ptr : felt*}():
alloc_locals
local original_number : felt # A local variable
local a_special_number : felt
# A hint
%{
# Python code that processes the .json file
num = program_input['current_number']
ids.original_number = num
new_num = program_input['special_number']
ids.a_special_number = new_num
%}
serialize_word(original_number) # Make number an output
serialize_word(a_special_number)
return ()
end
Execute program
Now compile program to produce program_compiled.json
:
cairo-compile program.cairo --output='program_compiled.json'
Now run the program, using the compiled program_compiled.json
file:
cairo-run --program=program_compiled.json \
--print_output --print_info --relocate_prints --tracer
Confirm that the program output matches the output below:
300
556
To explore the program structure and debug, visit the tracer at http://localhost:8100/.
Deploy program
The program can be sent to a public Ethereum testnet (Ropsten) using SHARP. Run the following command to send the program to SHARP for proof generation and fact registration:
cairo-sharp submit --source program.cairo \
--program_input input.json
The above command will produce a Fact
(a hash of the outputs and the program hash). Any Ropsten
application contract or user can now query the isValid(Fact)
read method of the deployed
Fact Registry.
If the result is True
, then that application contract or user can be confident that:
- The Cairo program has computational integrity (validity).
- The inputs used in that program truly produced those outputs (correctness).
An application can be built by designing and deploying an Ethereum contract that:
- Stores the
program_hash
permanently to be able to recognise this unique Cairo program. - Accepts the outputs that come from that Cairo program.
- Uses those values in some way.
Application Design
That application contract needs to have a method that performs particular steps.
The steps and some corresponding Solidity examples are outlined below for a function
called updateState()
, which:
Accepts, as an argument, the Cairo program outputs programOutput
.
function updateState(uint256[] memory programOutput) public {}
.
Computes the output hash, outputHash
.
bytes32 outputHash = keccak256(abi.encodePacked(programOutput));
.
Computes the fact
, which is a keccak hash of the Pedersen hash of the program
and the program outputs. The program hash is permanent and can be retrieved from
contract storage.
bytes32 fact = keccak256(abi.encodePacked(cairoProgramHash_, outputHash));
.
Calls the Fact Registry
read method isValid(fact)
to determine if the proof
should be accepted. The address of the verifier is permanent and can be
retrieved from contract storage.
require(cairoVerifier_.isValid(fact), "MISSING_CAIRO_PROOF");
.
Updates the application state applicationState_
, accessing the program outputs by index,
according to the specific application.
applicationState_ = programOutput[1]
.
In this way, a user may interact with a Cairo program to ultimately execute a change on Ethereum without using large amounts of expensive storage or computation.
Application Deployment
A solidity contract CairoApplication.sol
can be deployed to use the fact in the
FactRegistry
contract for its logic. That contract may have a function updateState()
which can be called by passing outputs from program.cairo
as arguments. The function
call would be a transaction that updates the state of the application to reflect the
state changes that the Cairo program created.
Application Use
A user, or agent on behalf of a user, can interact with the application by following the steps:
- Create an
inputs.json
file with custom values. - Obtain a copy of
program.cairo
. - Install Cairo and compile the program.
- Submit the program to SHARP for proving.
- Call
updateState()
function in theCairoApplication
contract.
These steps can be abstracted away from the user experience with the use of an interface and server backend.
Solidity code
Below is the Solidity code referenced in the above descriptions.
When the contract is deployed, the constructor
will need to be passed values that will
permanently live in the contract:
cairoProgramHash
, the enshrined Cairo program hash that the application will recognise.cairoVerifier
, the address of the Ropsten verifier, deployed by Starkware (0xf0EC41069A89595ADf5f27A4a90ff2DF30D83d2E
).initialNumber
, the “first number” the application will store.
This can be organised through smart contract deployment software such as
hardhat. These variables might be stored
in the deploy.js
that hardhat will use to send the contract to Ropsten.
pragma solidity ^0.5.2;
contract IFactRegistry {
/*
Returns true if the given fact was previously registered.
*/
function isValid(bytes32 fact)
external view
returns(bool);
}
contract SpecialNumber {
// The current special number
uint256 currentNumber_;
// The Cairo program hash.
uint256 cairoProgramHash_;
// The Cairo verifier.
IFactRegistry cairoVerifier_;
constructor(
uint256 cairoProgramHash,
address cairoVerifier,
uint256 initialNumber)
public
{
currentNumber_ = initialNumber;
cairoProgramHash_ = cairoProgramHash;
cairoVerifier_ = IFactRegistry(cairoVerifier);
}
function updateNumber(uint256[] memory programOutput)
public
{
// Ensure that a corresponding proof was verified.
bytes32 outputHash = keccak256(
abi.encodePacked(programOutput));
bytes32 fact = keccak256(
abi.encodePacked(cairoProgramHash_, outputHash));
require(
cairoVerifier_.isValid(fact),
"MISSING_CAIRO_PROOF");
// Ensure the output consistency with currentstate.
require(
programOutput.length == 2,
"INVALID_PROGRAM_OUTPUT");
require(
currentNumber_ == programOutput[0],
"MISSING_ORIGINAL_NUMBER");
require(
currentNumber_ != programOutput[1],
"NUMBER_MUST_BE_DIFFERENT");
// Update stored number with new number.
currentNumber_ = programOutput[1];
}
}
Caveats
This application is meant to illustrate the mechanics of an application, rather than to show the true power of using Cairo to increase throughput on Ethereum.
A more sophisticated application might involve the Cairo program accepting Ethereum addresses so that the proof could contain a record of who submitted a number. To to explore how scaling might be achieved, the app might instead accept multiple numbers. The Cairo program could then accept a collection of users and their numbers, thereby increasing the density of a proof from one number to many numbers. The proof cost would practically be the same cost and the cost per user would be a lot less.