Merge branch 'main' into schouhy/implement-privacy-preserving-tail-calls

This commit is contained in:
Sergio Chouhy 2025-11-07 19:21:43 -03:00
commit ffe2ae4e0b
3 changed files with 79 additions and 509 deletions

468
README.md
View File

@ -1,469 +1,33 @@
# nescience-testnet
This repo serves for Nescience Node testnet
This repo serves for Nescience testnet
For more details you can read [blogpost](https://vac.dev/rlog/Nescience-state-separation-architecture/)
For more details you can read [here](https://notes.status.im/Ya2wDpIyQquoiRiuEIM8hQ?view).
For more details on node functionality [here](https://www.notion.so/5-Testnet-initial-results-analysis-18e8f96fb65c808a835cc43b7a84cddf)
# Install dependencies
# How to run
Node and sequecer require Rust installation to build. Preferable latest stable version.
Rust can be installed as
Install build dependencies
- On Linux
```sh
apt install build-essential clang libssl-dev pkg-config
```
- On Mac
```sh
xcode-select --install
brew install pkg-config openssl
```
Install Rust
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
Node needs RISC0 toolchain to run.
It can be installed as
Install Risc0
```sh
curl -L https://risczero.com/install | bash
```
After that, before next step, you may need to restart your console, as script updates PATH variable. Next:
Then restart your shell and run
```sh
rzup install
```
After cloning this repository the following actions need to be done:
Entrypoints to node and sequencer are `node_runner` and `sequencer_runner`. Both of them have a configuration of similar manner. Path to configs need to be given into runner binaries as first arguent. No other arguments have to be given. We search given directory for files "node_config.json" for node and "sequencer_config.json" for sequencer.
With repository debug configs at `node_runner/configs/debug` and `sequencer_runner/configs/debug` are provided, you can use them, or modify as you wish.
For sequencer:
```yaml
{
"home": ".",
"override_rust_log": null,
"genesis_id": 1,
"is_genesis_random": true,
"max_num_tx_in_block": 20,
"block_create_timeout_millis": 10000,
"port": 3040
}
```
* "home" shows relative path to directory with datebase.
* "override_rust_log" sets env var "RUST_LOG" to achieve different log levels(if null, using present "RUST_LOG" value).
* "genesis_id" is id of genesis block.
* "is_genesis_random" - flag to randomise forst block.
* "max_num_tx_in_block" - transaction mempool limit.
* "block_create_timeout_millis" - block timeout.
* "port" - port, which sequencer will listen.
For node:
```yaml
{
"home": ".",
"override_rust_log": null,
"sequencer_addr": "http://127.0.0.1:3040",
"seq_poll_timeout_secs": 10,
"port": 3041
}
```
* "home" shows relative path to directory with datebase.
* "override_rust_log" sets env var "RUST_LOG" to achieve different log levels(if null, using present "RUST_LOG" value).
* "sequencer_addr" - address of sequencer.
* "seq_poll_timeout_secs" - polling interval on sequencer, in seconds.
* "port" - port, which sequencer will listen.
To run:
_FIRSTLY_ in sequencer_runner directory:
```sh
RUST_LOG=info cargo run <path-to-configs>
```
_SECONDLY_ in node_runner directory
```sh
RUST_LOG=info cargo run <path-to-configs>
```
# Node Public API
Node exposes public API with mutable and immutable methods to create and send transactions.
## Standards
Node supports JSON RPC 2.0 standard, details can be seen [there](https://www.jsonrpc.org/specification).
## API Structure
Right now API has only one endpoint for every request('/'), and JSON RPC 2.0 standard request structure is fairly simple
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": $string,
"params": $object
}
```
Response strucuture will look as follows:
Success:
```yaml
{
"jsonrpc": "2.0",
"result": $object,
"id": "dontcare"
}
```
There $number - integer or string "dontcare", $string - string and $object - is some JSON object.
## Methods
* get_block
Get block data for specific block number.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "get_block",
"params": {
"block_id": $number
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"block": $block
},
"id": $number_or_dontcare
}
```
There "block" field returns block for requested block id
* get_last_block
Get last block number.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "get_last_block",
"params": {}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"last_block": $number
},
"id": $number_or_dontcare
}
```
There "last_block" field returns number of last block
* write_register_account
Create new acccount with 0 public balance and no private UTXO.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "write_register_account",
"params": {}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"status": $string
},
"id": $number_or_dontcare
}
```
There "status" field shows address of generated account
* show_account_public_balance
Show account public balance, field "account_addr" can be taken from response in "write_register_account" request.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "show_account_public_balance",
"params": {
"account_addr": $string
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"addr": $string,
"balance": $number
},
"id": $number_or_dontcare
}
```
Fields in response is self-explanatory.
* write_deposit_public_balance
Deposit public balance into account. Any amount under u64::MAX can be deposited, can overflow.
Due to hashing process(transactions currently does not have randomization factor), we can not send two deposits with same amount to one account.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "write_deposit_public_balance",
"params": {
"account_addr": $string,
"amount": $number
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"status": "success"
},
"id": $number_or_dontcare
}
```
Fields in response is self-explanatory.
* write_mint_utxo
Mint private UTXO for account.
Due to hashing process(transactions currently does not have randomization factor), we can not send two mints with same amount to one account.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "write_mint_utxo",
"params": {
"account_addr": $string,
"amount": $number
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"status": "success",
"utxo": {
"asset": [$number],
"commitment_hash": $string,
"hash": $string
}
},
"id": $number_or_dontcare
}
```
There in "utxo" field "hash" is used for viewing purposes, field "commitment_hash" is used for sending purposes.
* show_account_utxo
Show UTXO data for account. "utxo_hash" there can be taken from "hash" field in response for "write_mint_utxo" request
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "show_account_utxo",
"params": {
"account_addr": $string,
"utxo_hash": $string
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"amount": $number,
"asset": [$number],
"hash": $string
},
"id": $number_or_dontcare
}
```
Fields in response is self-explanatory.
* write_send_utxo_private
Send utxo from one account private balance into another(need to be different) private balance.
Both parties are is hidden.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "write_send_utxo_private",
"params": {
"account_addr_sender": $string,
"account_addr_receiver": $string,
"utxo_hash": $string,
"utxo_commitment": $string
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"status": "success",
"utxo_result": {
"asset": [$number],
"commitment_hash": $string,
"hash": $string
}
},
"id": $number_or_dontcare
}
```
Be aware, that during this action old UTXO is nullified, hence can not be used anymore, even if present in owner private state.
* write_send_utxo_deshielded
Send utxo from one account private balance into another(not neccesary different account) public balance.
Sender is hidden.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "write_send_utxo_deshielded",
"params": {
"account_addr_sender": $string,
"account_addr_receiver": $string,
"utxo_hash": $string,
"utxo_commitment": $string
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"status": "success"
},
"id": $number_or_dontcare
}
```
Fields in response is self-explanatory.
* write_send_utxo_shielded
Send amount from one account public balance into another(not neccesary different account) private balance.
Receiver is hidden.
Request:
```yaml
{
"jsonrpc": "2.0",
"id": $number_or_dontcare,
"method": "write_send_utxo_shielded",
"params": {
"account_addr_sender": $string,
"account_addr_receiver": $string,
"amount": $number
}
}
```
Response:
```yaml
{
"jsonrpc": "2.0",
"result": {
"status": "success",
"utxo_result": {
"asset": [$number],
"commitment_hash": $string,
"hash": $string
}
},
"id": $number_or_dontcare
}
```
Fields in response is self-explanatory.

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
use crate::account::{Account, AccountWithMetadata};
use risc0_zkvm::serde::Deserializer;
use risc0_zkvm::{DeserializeOwned, guest::env};
@ -71,30 +73,35 @@ pub fn validate_execution(
post_states: &[Account],
executing_program_id: ProgramId,
) -> bool {
// 1. Lengths must match
// 1. Check account ids are all different
if !validate_uniqueness_of_account_ids(pre_states) {
return false;
}
// 2. Lengths must match
if pre_states.len() != post_states.len() {
return false;
}
for (pre, post) in pre_states.iter().zip(post_states) {
// 2. Nonce must remain unchanged
// 3. Nonce must remain unchanged
if pre.account.nonce != post.nonce {
return false;
}
// 3. Program ownership changes are not allowed
// 4. Program ownership changes are not allowed
if pre.account.program_owner != post.program_owner {
return false;
}
let account_program_owner = pre.account.program_owner;
// 4. Decreasing balance only allowed if owned by executing program
// 5. Decreasing balance only allowed if owned by executing program
if post.balance < pre.account.balance && account_program_owner != executing_program_id {
return false;
}
// 5. Data changes only allowed if owned by executing program or if account pre state has
// 6. Data changes only allowed if owned by executing program or if account pre state has
// default values
if pre.account.data != post.data
&& pre.account != Account::default()
@ -103,13 +110,13 @@ pub fn validate_execution(
return false;
}
// 6. If a post state has default program owner, the pre state must have been a default account
// 7. If a post state has default program owner, the pre state must have been a default account
if post.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
return false;
}
}
// 7. Total balance is preserved
// 8. Total balance is preserved
let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum();
let total_balance_post_states: u128 = post_states.iter().map(|post| post.balance).sum();
if total_balance_pre_states != total_balance_post_states {
@ -118,3 +125,14 @@ pub fn validate_execution(
true
}
fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool {
let number_of_accounts = pre_states.len();
let number_of_account_ids = pre_states
.iter()
.map(|account| account.account_id.clone())
.collect::<HashSet<_>>()
.len();
number_of_accounts == number_of_account_ids
}

View File

@ -3,7 +3,12 @@ use std::collections::HashSet;
use risc0_zkvm::{guest::env, serde::to_vec};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramId, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, DUMMY_COMMITMENT_HASH
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme,
Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
account::{Account, AccountId, AccountWithMetadata},
compute_digest_for_path,
encryption::Ciphertext,
program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution},
};
fn main() {
@ -16,43 +21,12 @@ fn main() {
program_id,
} = env::read();
// These lists will be the public outputs of this circuit
// and will be populated next.
let mut public_pre_states: Vec<AccountWithMetadata> = Vec::new();
let mut public_post_states: Vec<Account> = Vec::new();
let mut ciphertexts: Vec<Ciphertext> = Vec::new();
let mut new_commitments: Vec<Commitment> = Vec::new();
let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new();
// TODO: WIP
let program_output = program_outputs[0].clone();
for program_output in program_outputs.iter() {
// Check that `program_output` is consistent with the execution of the corresponding program.
env::verify(program_id, &to_vec(program_output).unwrap()).unwrap();
}
// Check that `program_output` is consistent with the execution of the corresponding program.
env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap();
if private_nonces_iter.next().is_some() {
panic!("Too many nonces.");
}
if private_keys_iter.next().is_some() {
panic!("Too many private account keys.");
}
if private_auth_iter.next().is_some() {
panic!("Too many private account authentication keys.");
}
let output = PrivacyPreservingCircuitOutput {
public_pre_states,
public_post_states,
ciphertexts,
new_commitments,
new_nullifiers,
};
env::commit(&output);
}
fn validate_program_execution(program_output: &ProgramOutput, program_id: ProgramId) {
let ProgramOutput {
pre_states,
post_states,
@ -64,11 +38,6 @@ fn validate_program_execution(program_output: &ProgramOutput, program_id: Progra
panic!("Privacy preserving transactions do not support yet chained calls.")
}
// Check that there are no repeated account ids
if !validate_uniqueness_of_account_ids(&pre_states) {
panic!("Repeated account ids found")
}
// Check that the program is well behaved.
// See the # Programs section for the definition of the `validate_execution` method.
if !validate_execution(&pre_states, &post_states, program_id) {
@ -80,6 +49,14 @@ fn validate_program_execution(program_output: &ProgramOutput, program_id: Progra
panic!("Invalid visibility mask length");
}
// These lists will be the public outputs of this circuit
// and will be populated next.
let mut public_pre_states: Vec<AccountWithMetadata> = Vec::new();
let mut public_post_states: Vec<Account> = Vec::new();
let mut ciphertexts: Vec<Ciphertext> = Vec::new();
let mut new_commitments: Vec<Commitment> = Vec::new();
let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new();
let mut private_nonces_iter = private_account_nonces.iter();
let mut private_keys_iter = private_account_keys.iter();
let mut private_auth_iter = private_account_auth.iter();
@ -173,15 +150,26 @@ fn validate_program_execution(program_output: &ProgramOutput, program_id: Progra
_ => panic!("Invalid visibility mask value"),
}
}
}
fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool {
let number_of_accounts = pre_states.len();
let number_of_account_ids = pre_states
.iter()
.map(|account| account.account_id.clone())
.collect::<HashSet<_>>()
.len();
if private_nonces_iter.next().is_some() {
panic!("Too many nonces.");
}
number_of_accounts == number_of_account_ids
if private_keys_iter.next().is_some() {
panic!("Too many private account keys.");
}
if private_auth_iter.next().is_some() {
panic!("Too many private account authentication keys.");
}
let output = PrivacyPreservingCircuitOutput {
public_pre_states,
public_post_states,
ciphertexts,
new_commitments,
new_nullifiers,
};
env::commit(&output);
}