README
This code is not owned by EY and EY provides no warranty and disclaims any and all liability for use of this code. Users must conduct their own diligence with respect to use for their purposes and any and all usage is on an as-is basis and at your own risk.
starlight ðŸŒ
Generate a zApp from a Solidity contract.
Introduction
zApps are zero-knowledge applications. They're like dApps (decentralised applications), but with privacy. zApps are tricky to write, but Solidity contracts are lovely to write. So why not try to write a zApp with Solidity?
starlight
helps developers do just this...
Write a Solidity contract
Add a few new privacy decorators to the contract (to get a 'Zolidity' contract)
Run
zappify
Get a fully working zApp in return
Solidity contract --> Zolidity contract --> zappify --> zApp
The main objective of this transpiler is to enable developers to quickly draft frameworks for zApps.
See here for the gitbook, and here for an enormously detailed explanation of how the transpiler works.
Warnings
This code is not owned by EY and EY provides no warranty and disclaims any and all liability for use of this code. Users must conduct their own diligence with respect to use for their purposes and any and all usage is on an as-is basis and at your own risk.
Note that this is an experimental prototype which is still under development. Not all Solidity syntax is currently supported. Here is guide to current functionality.
This code has not yet completed a security review and therefore we strongly recommend that you do not use it in production. We take no responsibility for any loss you may incur through the use of this code.
Due to its experimental nature, we strongly recommend that you do not use any zApps which are generated by this transpiler to transact items of material value.
Output zApps use the proof system Groth16 which is known to have vulnerabilities. Check if you can protect against them, or are protected by the zApps logic.
In the same way that Solidity does not protect against contract vulnerabilities, Starlight cannot protect against zApp vulnerabilities once they are written into the code. This compiler assumes knowledge of smart contract security.
Note on integer limits: Starlight compiles your Solidity contract into a zApp written in JavaScript. Unlike Solidity's uint256
(which supports integers up to 2^256-1), JavaScript's standard Number
type can only safely represent integers up to 9,007,199,254,740,991 (2^53 – 1), known as Number.MAX_SAFE_INTEGER
. This means that values above 2^53–1 are not fully supported in the JavaScript output, and large values will lose precision or behave unexpectedly. When designing your contracts, ensure that all values remain within the safe integer range for JavaScript.
Requirements
To run the zappify
command:
Node.js v15 or higher (Known issues with v13 and v18).
To run the resulting zApp:
Node.js v15 or higher.
Docker (with 16GB RAM recommended)
Mac and Linux machines with at least 16GB of memory and 10GB of disk space are supported.
Quick User Guide
1. Creating a Zolidity (.zol
) contract from a standard Solidity (.sol
) contract
.zol
) contract from a standard Solidity (.sol
) contractZolidity decorators: secret
, known
, unknown
, encrypt
, and re-initialisable
In Starlight, each secret variable is assigned an owner. The owner holds a secret key that allows them to access the private value and update that variable. Secret variables are stored on-chain via commitments, and the original data (preimage) is kept locally by the owner.
Zolidity extends Solidity with the following decorators:
secret
: Marks a variable or parameter as private, so its value is hidden from other users and only accessible to its owner.known
: Indicates that only the variable owner can modify the variable. By default all variables are known.unknown
: Allows any user to increment the variable, not just the owner.encrypt
: Use when creating a commitment for another user (e.g., withunknown
). Ensures, via zk-proofs, that a correct encryption of the commitment pre-image is broadcast and can be decrypted by the owner so that the commitment can be used in the future.re-initialisable
: Allows a secret variable to be re-initialised, if later in the contract it is burned.
Start with a standard Solidity contract, for example:
// SPDX-License-Identifier: CC0
pragma solidity ^0.8.0;
contract Assign {
uint256 private a;
function assign(uint256 value) public {
a = value;
}
}
To convert your Solidity contract to a Zolidity contract, insert the secret
keyword in front of the declaration of any state variable, local variable, or function parameter you want to keep private:
// SPDX-License-Identifier: CC0
pragma solidity ^0.8.0;
contract Assign {
secret uint256 private a; // <--- secret
function assign(secret uint256 value) public { // <--- secret
a = value;
}
}
Example: Charity contract using unknown
and known
Here's a simple Zolidity contract for a charity, where anyone can donate but only the admin can withdraw. The variable balance is marked with unknown before the incrementation in donate(), to mark that anyone can call donate and increment balance. Only the owner can decrement balance in the withdraw function.
// SPDX-License-Identifier: CC0
pragma solidity ^0.8.0;
contract CharityPot {
secret uint256 private balance;
address public admin;
constructor(address _admin) {
admin = _admin;
}
// Anyone can donate to the charity
function donate(secret uint256 amount) public {
unknown balance += amount; // <--- anyone can increment
}
// Only the admin can withdraw funds
function withdraw(secret uint256 amount) public {
require(msg.sender == admin, "Only admin can withdraw");
balance -= amount; // <--- only admin can decrement
}
}
Example: Using the encrypt
decorator
Sometimes, a commitment is created for a secret variable that is owned by another user. For example, when using the unknown
tag (as explained above), or with address variables — where ownership is tied to the address and can change when the address changes. To use a commitment, the owner needs the commitment pre-image, but they may not have access to it if they did not create the commitment themselves. The encrypt
tag solves this by encrypting and broadcasting the commitment pre-image, guaranteeing via the zk proofs that the ciphertext is correctly formed so the new owner can access and use the commitment. For example:
// SPDX-License-Identifier: CC0
pragma solidity ^0.8.0;
contract Assign {
secret uint256 private a; // <--- secret
function assign(secret uint256 value) public { // <--- secret
encrypt unknown a += value; // <--- guarantees the new owner can use the new commitment related to this state update
}
}
Example: Using the re-initisalisable
decorator
In the contract below, the owner of tokenOwners[tokenId]
is the address stored as its mapping value. When withdraw
is called, the commitment is nullified and the new value is address(0)
, so no new commitment is created. This creates a problem if deposit is called again: how can a new owner provide a nullifier if the token was previously owned? The re-initialisable
decorator solves this by allowing the variable to be re-initialised in deposit
.
secret mapping(uint256 => address) public tokenOwners;
IERC721 public erc721;
constructor(address _erc721) {
erc721 = IERC721(_erc721);
}
function deposit(uint tokenId) public {
bool success = erc721.transferFrom(msg.sender, address(this), tokenId);
require(success == true);
reinitialisable tokenOwners[tokenId] = msg.sender;
}
function transfer(secret address recipient, secret uint256 tokenId) public {
require(tokenOwners[tokenId] == msg.sender, "Youre not the owner of this token.");
tokenOwners[tokenId] = recipient;
}
function withdraw(uint256 tokenId) public {
require(tokenOwners[tokenId] == msg.sender, "Youre not the owner of this token.");
tokenOwners[tokenId] = address(0);
bool success = erc721.transferFrom(address(this), msg.sender, tokenId);
require(success == true);
}
Save these decorated contracts with a .zol
extension (for example, Assign.zol
and CharityPot.zol
). These files are now Zolidity contracts.
2. Using zappify
to transpile Zolidity to a standalone zApp
zappify
to transpile Zolidity to a standalone zAppOnce you have your Zolidity (.zol
) contract, run the following command to generate a standalone zApp:
zappify -i <./path/to/Assign.zol>
This will transpile your contract into an entire standalone zApp, outputting it to the ./zapps/
directory by default.
Install via npm
Starlight is now available on github's npm package repository! To install, you need to make sure that npm
is looking in that registry, not the default npmjs
:
npm config set @eyblockchain:registry https://npm.pkg.github.com
<-- this sets any @eyblockchain
packages to be installed from the github registry
npm i @eyblockchain/starlight@<latest-version>
Then you can import the zappify
command like so:
import { zappify } from "@eyblockchain/starlight";
The only required input for zappify
is the file path of the .zol
file you'd like to compile, e.g. zappify('./contracts/myContract.zol')
. You can optionally add a specific zApp name and the output directory:
zappify('./contracts/myContract.zol', 'myzApp', './test_zApps')
Then it works just as described in the below section Run.
Troubleshooting
Since we push the npm package to github's repository, you may need to specify the package location in your project's .npmrc
file and authenticate to github. Try adding something like:
@eyblockchain:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=<your_github_token>
...and re-run the install command, or manually add "@eyblockchain/starlight"
to your package.json
.
Install via clone
To install via github:
Clone the repo.
cd starlight
./bin/start
(You might need to run chmod +x ./bin/start for permission to execute the newly created shell scripts)
This runs tsc
and npm i -g ./
which will create a symlink to your node.js bin, allowing you to run the commands specified in the "bin":
field of the package.json
; namely the zappify
command.
Run
zappify -i ./path/to/MyZolidityContract.zol
... converts a Zolidity contract into a zApp. By default, the zApp is output to a ./zapps/
folder.
CLI options
--input <./path/to/contract.zol>
-i
Specify an input contract file with a .zol
extension.
--output <./custom/output/dir/>
-o
Specify an output directory for the zApp. By default, the zApp is output to a ./zapps/
folder.
--zapp-name <customZappName>
-z
Otherwise files get output to a folder with name matching that of the input file.
--log-level <debug>
-
Specify a Winston log level type.
--help
-h
CLI help.
Troubleshooting
Installation
If the zappify
command isn't working, try the Install steps again. You might need to try npm i --force -g ./
.
In very rare cases, you might need to navigate to your node.js innards and delete zappify from the bin
and lib/node_modules
. To find where your npm lib is, type npm
and it will tell you the path.
E.g.:
~/.nvm/versions/node/v15.0.1/lib/node_modules/npm
^
lib
^
bin is also at this level
Compilation
If you find errors to do with 'knownness' or 'unknownness', try to mark incrementations. If you have any secret states which can be incremented by other users, mark those incrementations as unknown
(since the value may be unknown to the caller).
// SPDX-License-Identifier: CC0
pragma solidity ^0.8.0;
contract Assign {
secret uint256 private a; // <--- secret
function add(secret uint256 value) public { // <--- secret
unknown a += value; // <--- may be unknown to the caller
}
function remove(secret uint256 value) public { // <--- secret
a -= value;
}
}
However, if you want the incrementation to only be completed by the secret owner, mark it as known:
// SPDX-License-Identifier: CC0
pragma solidity ^0.8.0;
contract Assign {
secret uint256 private a; // <--- secret
function add(secret uint256 value) public { // <--- secret
known a += value; // <--- must be known to the caller
}
function remove(secret uint256 value) public { // <--- secret
a -= value;
}
}
Failing to mark incrementations will throw an error, because the transpiler won't know what to do with the state. See the write up for more details.
If your input contract has any external imports, make sure those are stored in the ./contracts
directory (in root) and compile with solc
0.8.0.
Developer
Testing
full zapp
To test an entire zApp, which has been output by the transpiler:
Having already run zappify
, the newly-created zApp will be located in the output dir you specified (or in a dir called ./zapps
, by default). Step into that directory:
cd zapps/MyContract/
Install dependencies:
npm install
Start docker.
(At this stage, you might need to run chmod +x ./bin/setup && chmod +x ./bin/startup
for permission to execute the newly created shell scripts)
Run trusted setups on all circuit files:
./bin/setup
<-- this can take quite a while!
After this command, you're ready to use the zApp. You can either run a Starlight-generated test, or call the included APIs for each function.
For the compiled test see below:
npm test
<-- you may need to edit the test file (zapps/MyContract/orchestration/test.mjs
) with appropriate parameters before running!
npm run retest
<-- for any subsequent test runs
It's impossible for a transpiler to tell which order functions must be called in, or the range of inputs that will work. Don't worry - If you know how to test the input Zolidity contract, you'll know how to test the zApp. The signatures of the original functions are the same as the output nodejs functions. There are instructions in the output test.mjs
on how to edit it.
All the above use Docker in the background. If you'd like to see the Docker logging, run docker-compose -f docker-compose.zapp.yml up
in another window before running.
For using APIs:
npm start
<-- this starts up the express
app and exposes the APIs
Each route corresponds to a function in your original contract. Using the Assign.zol
example above, the output zApp would have an endpoint for add
and remove
. By default, requests can be POSTed to http://localhost:3000/<function-name>
in JSON format. Each variable should be named like the original parameter. For example, if you wanted to call add
with the value 10, send:
{
value: 10
}
to http://localhost:3000/add
. This would (privately) increase the value of secret state a
by 10, and return the shield contract's transaction.
To access your secret data locally, you can look at all the commitments you have by sending a GET request to http://localhost:3000/getAllCommitments
.
You can also filter these commitments by variable name. Using the example above, if you wanted to see all commitments you have representing the variable a
, send:
{
"name": "a"
}
as a GET request to http://localhost:3000/getCommitmentsByVariableName
.
If the commitment database that stores commitment preimages is lost you can restore the DB (by decrypting the onchain encryptions of the preimages) by sending a POST request to http://localhost:3000/backupDataRetriever
. You can also restore only the commitments with a specific variable name. For example, if you want to restore commitments representing variable a
, send:
{
"name": "a"
}
as a POST request to http://localhost:3000/backupVariable
.
Private Swap Contract
The Swap contract enables private atomic swaps of tokens between two parties using shared secrets and zk-based confidentiality. Here's how it works:
Secret State: Uses secret annotations for private values (e.g. balances, token ownership, and swap data).
Shared Secret: A unique identifier derived from the private keys of both parties (through Diffie-Hellman). This is used to securely index swap proposals.
Swap Flow:
Both users deposit tokens into the contract.
One user initiates a swap proposal using a shared address.
The counterparty accepts (completes) or cancels (quits) the swap.
Compile and Prepare Circuits:
$ zappify -i test/contracts/user-friendly-tests/Swap.zol
$ cd zapps/Swap
$ npm install
$ sh bin/setup # May take a while to compile circuits
$ sh bin/startup-double
This starts two orchestration servers running the Swap contract that can be reached on ports 3000 and 3001 respectively. Let's say user A runs its server on port 3001 and user B on 3000.
For user A to initiate a swap with B, it needs a shared secret key derived from its public key (recipientPubKey
) and the private key from user B. This is how B would compute the shared secret using the public key from A, send:
{
"recipientPubKey": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
}
as a POST request to http://localhost:3001/getSharedKeys
.
Response:
{
"SharedKeys": {
"_hex": "0x3e574c310f7bc1657f7e0e127690a8f885e4bcd42c15489a332ed9a6658bfef6"
}
}
Use this shared key as the sharedAddress
in swap interactions.
Deposit Tokens
Each party deposits tokens they intend to trade.
User A sends:
{
"tokenId": 1,
"amount": 100
}
as a POST request to http://localhost:3001/deposit
.
User B sends:
{
"tokenId": 2,
"amount": 100
}
as a POST request to http://localhost:3000/deposit
.
Initiate Swap
User A proposes a swap to the shared address, they send:
{
"sharedAddress": "0x3e574c310f7bc1657f7e0e127690a8f885e4bcd42c15489a332ed9a6658bfef6",
"amountSent": 30,
"tokenIdSent": 1,
"tokenIdRecieved": 2
}
as a POST request to http://localhost:3001/startSwap
.
This deducts 30 tokens from User A and locks token 1 for the proposed swap.
Complete Swap
User B accepts the swap with a matching offer, they send:
{
"sharedAddress": "0x3e574c310f7bc1657f7e0e127690a8f885e4bcd42c15489a332ed9a6658bfef6",
"counterParty": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"tokenIdSent": 2,
"tokenIdRecieved": 1,
"amountRecieved": 30
}
as a POST request to http://localhost:3000/completeSwap
.
The contract validates the match and executes the atomic swap:
Transfers token ownership.
Updates balances.
Clears swap state.
Cancel Swap
If the second party doesn’t agree, User A can cancel the proposal, they send:
{
"sharedAddress": "0x3e574c310f7bc1657f7e0e127690a8f885e4bcd42c15489a332ed9a6658bfef6",
"amountSent": 30,
"tokenIdSent": 1
}
as a POST request to http://localhost:3001/quitSwap
.
This reverts the proposal and refunds locked assets to the proposer.
Constructor inputs
When running
npm start
, you will be prompted for any user-supplied constructor inputs.For imported contract addresses:
If you enter
"NA"
, Starlight will deploy a fresh instance of the contract and use its address as the constructor input.Note: This auto-deployment is only supported for a fixed set of contracts stored here.
Deploy on public testnets
Apart from local ganache instance, Starlight output zapps can now be deployed in Sepolia, Goerli, Polygon Amoy, Polygon Cardona, Polygon zkEVM mainnet and Base mainnet as cli options. Connection to Sepolia and Goerli are made through infura endpoints, for Polygon Amoy and Base mainnet via Alchemy and for Polygon Cardona and Polygon zkEVM mainnet via Blast.
The configuration can be done during ./bin/setup
phase in the following way.
./bin/setup -n network -a account -k privatekey -m "12-word mnemonic" -r APIKey
CLI options
-n
Network : Specify testnet you want to connect to. possible options: amoy/ sepolia/ goerli/ cardona/ zkEVM/ base-mainnet
-a
Ethereum address
-k
Private key of above ethereum address
-m
-
12 letter mnemonic passphrase
-r
API key or APPID of endpoint
-s
ZKP setup flag, defaults to yes. If you had already created zkp keys before and just want to configure deployment infrastructure, pass -s n
Testing circuits
cd ./path/to/myCircuit.zok
docker run -v $PWD:/app/code -ti ghcr.io/eyblockchain/zokrates-worker-updated:latest /bin/bash
./zokrates compile -i code/circuits/myCircuit.zok
<-- it should compile
Zokrates Worker
Starlight uses containerised zokrates from zokrates-worker-starlight. Currently, all circuit compilation, setup, and proof generation use Zokrates version 0.8.8.
Contributing
See here for our contribution guidelines.
License
Notes:
This repository contains some modules of code and portions of code from Babel (https://github.com/babel/babel). All such code has been modified for use in this repository. Babel is MIT licensed. See LICENSE file.
This repository contains some portions of code from The Super Tiny Compiler (https://github.com/jamiebuilds/the-super-tiny-compiler). All such portions of code have been modified for use in this repository. The Super Tiny Compiler is CC-BY-4.0 licensed. See LICENSE file.
Acknowledgements
Authors:
MirandaWood
iAmMichaelConnor
Important packages:
Inspirational works:
Last updated