aztec3-speccing-book
This page has been published to docs.aztec.network. Please update here
See here for draft milestones.
See here for an unfinished (but maybe helpful) contract deployment sequence diagram, and other tabs for other useful diagrams.
See here for more on Contract Creation.
As a developer, I can write a Noir++ contract scope, which contains a collection of pure functions (circuits) (no state variables yet).
I can type
noir my_contract.nr
to compile a Noir contract to a JSON abi (or, hand craft the JSON abi).I can type
aztec-cli deploy_contract my_contract_abi.json
and have a contract deployed to my local network.I can type
aztec-cli get_code address
to verify my contract has been deployed succesfully.
The aim is to keep the implementation as simple as possible. We want to achieve the appropriate state shift on L1, and avoid anything that feels like unnecessary boiler-plate, or just "vanilla engineering". We're avoiding databases, network io, optimisations etc. Most of the code in this milestone should be directly related to computing and making the requisite state shift, otherwise we maybe going out of scope.
It'll basically require the infrustructure needed to maintain the contracts tree, and the ability to construct and insert a leaf into it:
Taken from the diagrams linked-to at the top of the page.
Note: some other data will also need to be broadcast as calldata on-chain, for data availability purposes. E.g. the ACIR opcodes of each newly-deployed contract.
Greyed out components don't need to exist at all for this milestone, but basically everything will need to exist in some capacity (even if some parts are heavily simulated).
This spec uses snake_case
for it's naming, however in the world of TypeScript we should adapt these names as appropriate to be camelCase
.
Although we often speak about "JSON RPC", this is a transport layer concept, and is not actually needed for this milestone.
The various services and modules should be defined as TypeScript interfaces. Functions that take arguments and return results. We will start by just implementing these interfaces directly, and running the stack as a single monolith. We can then implement them as individual services a bit later by just autogenerating the JSON RPC http client code and server handlers. (We have this pattern already between e.g. falafel and halloumi. Halloumi can just be run as a module within the falafel process, or run as a separate service).
There will be no proofs generated in early milestones. Functions that would normally build an actual proof can be skipped (or be noops). The simulators should produce output that is identical to a proof output, just without the actual proof data (e.g. just the public_inputs
). The kernel simulator and rollup simulator, will use the exact same C++ circuit logic, only the simulator will use native machine types so it's fast.
The lowest level system state is represented by one or more streams of data. In AC this was the calldata and the off-chain data. A3 will likely have it's state also represented as more than one stream of data. For this milestone we could just adopt a similar separation of data, although longer term some state might make it's way into danksharding blobs, and maybe some state won't go to the ethereum network at all.
Regardless, there should be a separation of concern between the stream of data, and how/where it's stored. Data should be stored unprocessed, in an implementation of a simple key-value store interface. So e.g. there maybe a component that when pointed at the rollup contract, stores the calldata for each rollup in a kv impl that just saves it to a file named by rollup number.
A higher level component can take those multiple streams of data, and reconcile them to produce another single stream of data, the full data to describe a single rollup. These tasks will be handled by the data archiver, allowing querying of the data archiver for rollups > n.
The Private Client will have different methods for acquiring the data needed to produce its own data streams (one stream of data per account, effectively filtering the full chain state down to just the events relevant to that account).
The output streams of these technologies are the same, making them interchangeable. This stream of data represents the state shifts from the perspective of a single account. For this milestone we will just brute force.
There are a few considerations to make when processing state:
Think of something like "Go channels" in terms of design (our MemoryFifo was written to leverage this pattern). Whereas a stream is often thought of as a buffered stream of bytes, channels are more like a buffered stream of messages. The SDK in AC has an example of how to do this. The BlockDownloader is told to start from a certain block number. It will build up an internal buffer of rollups in its "channel" (queue) till it hits some limit. A consumer calls getRollup
which will block until a rollup is returned. Thus the consumer can have simple "synchronous" control flow loop. The code can also then be run naturally against a fixed size data store (it would just never block). It can act as a simple transformer, ingesting a directory of files and outputing another directory of files. This should also make isolated unit testing simple.
From an account perspective, as we process the accounts data stream, we will need to process the data through a simulator to execute contract specific filtering logic. This kind of transform changes the stream of data events into a snapshot. The final data representation thus maybe a set of key-values where a key is a storage slot, and a value maybe e.g. the sum of all utxos (the accounts balance).
The takeaway of this is to not get carried away with high level databases and indexes at this point, but to think of data as streams of data with simple low level representations. The transformations should be simple, modular, easily testable, and reusable in different environments (via storage abstraction).
It's important to define some key concepts as interfaces. These can be categorised as either data, or logic based interfaces.
Data interfaces are about retrieving and/or updating state. By using the right abstractions, it will be possible to:
SiblingPathSource
that just queries the remote PublicClient
for a contracts sibling path.Logic interfaces are about executing specific bits of functionality. By using the right abstrations it will be possible to:
Responsibilities:
Interface:
create_account(): PublicKey
get_accounts(): PublicKey[]
sign_tx_request(account_pub_key, tx_request): Signature
tx_request
.decrypt(account_pub_key, data, eph_pub_key): Buffer
Responsibilities:
KeyStore
.Interface:
create_account(): PublicKey
KeyStore
get_accounts(): PublicKey[]
KeyStore
sign_tx_request(account_pub_key, tx_request): Signature
KeyStore
simulate_tx(tx_request): SimulationResponse
public_inputs
of the kernel circuit.create_tx(tx_request): Tx
send_tx(tx): void
PublicClient
.Responsibilties:
SiblingPathSource
).RollupSource
).P2PClient
.Interface:
send_tx(tx)
send_tx
.get_rollups(from, take)
get_sibling_path(tree_id, index): SiblingPath
get_storage_slot(address): Buffer32
Responsibilities:
PublicClient
.P2PClient
instances in the network.P2PClient
instances in the network.Interface:
send_tx(tx)
tx
, if valid, add to local pool and forward to other peers.
get_txs(): Tx[]
send_proof_request
get_proof_requests
Responsibilities:
tree_id
at the given leaf index
.WorldStateSynchroniser
.Interface:
get_sibling_path(tree_id, index): SiblingPath
Responsibilities:
Rollup
data.Interface:
get_latest_rollup_id()
get_rollups(from, take): Rollup[]
Responsiblities:
Interface:
process_rollup(rollup_data): boolean
Responsibilities:
SequencerClient
instances for rolling up.Interface:
send_tx(tx): boolean
Responsibilities:
Interface:
get_txs(): Tx[]
These tasks are lower priority than providing a handcrafted ABI.
Design a Noir Contract ABI, similar to a Solidity ABI which is output by Solc (see here). It might include for each function:
Provides the aztec-cli
binary for interacting with network from the command line. It's a thin wrapper around aztec.js
to make calls out to the client services. It's stateless.
Should provide sensible api, that provides the following functionalities. Start by writing the single e2e test that will check for the successful contract deployment. We don't need to get this perfect, but think hard about making the process feel very natural to a user of the library.
aztec.js should always be stateless. It offers the ability to interact with stateful systems such as the public and private clients.
The analogous AC component would be the AztecSdk (wraps the CoreSdk which is more analogous to the private client).
create_account
on Wallet.Contract
instance (similar to web3.js), given a path to a Noir Contract ABI.tx_request
by calling e.g. contract.get_deployment_request(constructor_args)
.sign_tx_request(tx_request)
to get signature.simulate_tx(signed_tx_request)
on the Private Client. In future this would help compute gas, for now we won't actually return gas (it's hard). Returns success or failure, so client knows if it should proceed, and computed kernel circuit public outputs.create_tx(signed_tx_request)
on Private Client to produce kernel proof. Can be skipped for this milestone. In future should be able to generate either mock (few constraints) or real proof.send_tx(tx)
on Private Client to send kernel proof to Public Client. Get back a receipt.get_tx_receipt
on the Private Client.L1 client library. No ethers.js. Uses our existing and new L1 client code.
Interfaces Implemented:
KeyStore
Implementation notes for this milestone:
Implements:
AztecRpcClient
(The server is a client, when used directly)Injected:
KeyStore
Implementation notes for this milestone:
PublicClient
on startup.SiblingPathSource
that queries the paths from the PublicClient
.simulate_tx
create_tx
simulate_tx
.tx
that can be sent to send_tx
with everything but actual proof data.send_tx
Implements:
PublicClient
Injected:
P2PClient
RollupSource
MerkleTreeDb
Implementation notes for this milestone:
WorldStateSynchroniser
will ingest rollups from the RollupSource
and maintain an up-to-date MerkleTreeDb
.Implements:
P2PClient
Injected:
RollupSource
MerkleTreeDb
Implementation notes for this milestone:
Responsibilities:
RollupPublisher
.For this milestone, the sequencer will just simulate and publish a 1x1 rollup and publish it to L1.
Implements:
SiblingPathSource
Implementation notes for this milestone:
Closest analogous component in AC is the WorldStateDb
in bb.js. We can configure the backing store (probably leveldb) to be an in-memory only store. We don't need persistence, we will rebuild the tree at startup. This will ensure we have appropriate sync-from-zero behaviour.
Responsibilities:
Interface:
get_root
get_num_leaves
get_sibling_path
append_leaves
Injected:
RollupSource
MerkleTreeDb
Responsilbities:
RollupSource
and updates trees in the MerkleTreeDb
.Implements:
RollupSource
Responsibilities:
provessRollup
and offchainData
for this milestone.)Rollup
.Interface:
get_latest_rollup_id
get_rollups(from, take)
Implements:
RollupReceiver
Implementation notes for this milestone:
RollupPublisher
.Interface:
processRollup(proofData, l1Data)
offchainData(data)
Implementation notes for this milestone:
The rollup contract in AC holds data in two places that are reconciled when processing the rollup. The calldata passed into the processRollup
function, and the calldata passed into the offchainData
function. They were separated, as only the data given to processRollup
is needed to ensure rollup liveness (i.e. if the "offchain data" were not published, we could still produce rollups). Ultimately the plan was to move the offchain data off of L1 altogether to reduce costs.
For this milestone, we will want to leverage a similar separation of data. The data to processData
will consist of:
proofData
- The proof data to be fed to the verifier. Likely will have a public inputs that represent:
l1Data
- Data that is needed for making L2 <> L1 function calls.For logic:
For offchainData
:
witness
: Any value within the circuit, by default these are private, and thus not visible on the proof. Some witnesses may be made public (visible on the proof), at which point they also become public_inputs
.[circuit_]input
: Data that is computed outside the circuit and is fed in at the beginning as a witness
. Note: if the context of this relating to a 'circuit' is already clear, we can omit circuit_
, as is done in the aztec3_circuits
repo.oracle_input
: Data that, during execution, is fetched from the oracle, and made a witness
.computed_public_input
: Data that is computed within the circuit, and set to be a public_input
.public_input
: Data that is set to be public by the circuit. This could include some circuit_input
data, and/or some computed_public_input
data, and/or some oracle_input
data.See diagram: https://miro.com/app/board/uXjVPlafJWM=/