mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-16 08:03:14 +00:00
feat: move initial_accounts and initial_commitments into genesis
This commit is contained in:
parent
0b070e5ad2
commit
9c2d353ebd
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -2794,6 +2794,14 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "genesis_supply_account_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nssa_core",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@ -3611,7 +3619,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"testcontainers",
|
||||
"testnet_initial_state",
|
||||
"token_core",
|
||||
"tokio",
|
||||
"url",
|
||||
@ -3998,7 +4005,6 @@ dependencies = [
|
||||
"hmac-sha512",
|
||||
"itertools 0.14.0",
|
||||
"k256",
|
||||
"log",
|
||||
"nssa",
|
||||
"nssa_core",
|
||||
"rand 0.8.5",
|
||||
@ -6097,6 +6103,7 @@ dependencies = [
|
||||
"ata_core",
|
||||
"ata_program",
|
||||
"clock_core",
|
||||
"genesis_supply_account_core",
|
||||
"nssa_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
@ -7347,8 +7354,10 @@ dependencies = [
|
||||
"chrono",
|
||||
"common",
|
||||
"futures",
|
||||
"genesis_supply_account_core",
|
||||
"humantime-serde",
|
||||
"jsonrpsee",
|
||||
"key_protocol",
|
||||
"log",
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-key-management-system-service",
|
||||
|
||||
@ -20,6 +20,7 @@ members = [
|
||||
"programs/token",
|
||||
"programs/associated_token_account/core",
|
||||
"programs/associated_token_account",
|
||||
"programs/genesis_supply_account/core",
|
||||
"sequencer/core",
|
||||
"sequencer/service",
|
||||
"sequencer/service/protocol",
|
||||
@ -64,6 +65,7 @@ amm_core = { path = "programs/amm/core" }
|
||||
amm_program = { path = "programs/amm" }
|
||||
ata_core = { path = "programs/associated_token_account/core" }
|
||||
ata_program = { path = "programs/associated_token_account" }
|
||||
genesis_supply_account_core = { path = "programs/genesis_supply_account/core" }
|
||||
test_program_methods = { path = "test_program_methods" }
|
||||
bedrock_client = { path = "bedrock_client" }
|
||||
testnet_initial_state = { path = "testnet_initial_state" }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/program_methods/genesis_supply_account.bin
Normal file
BIN
artifacts/program_methods/genesis_supply_account.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/genesis_supply_private_account.bin
Normal file
BIN
artifacts/program_methods/genesis_supply_private_account.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -76,7 +76,7 @@ impl NSSATransaction {
|
||||
) -> Result<ValidatedStateDiff, nssa::error::NssaError> {
|
||||
let diff = match self {
|
||||
Self::Public(tx) => {
|
||||
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp)
|
||||
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp, false)
|
||||
}
|
||||
Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction(
|
||||
tx, state, block_id, timestamp,
|
||||
|
||||
@ -16,117 +16,29 @@
|
||||
"node_url": "http://logos-blockchain-node-0:18080"
|
||||
},
|
||||
"indexer_rpc_url": "ws://indexer_service:8779",
|
||||
"initial_accounts": [
|
||||
"genesis": [
|
||||
{
|
||||
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
|
||||
"balance": 10000
|
||||
},
|
||||
{
|
||||
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
|
||||
"balance": 20000
|
||||
}
|
||||
],
|
||||
"initial_commitments": [
|
||||
{
|
||||
"npk":[
|
||||
177,
|
||||
64,
|
||||
1,
|
||||
11,
|
||||
87,
|
||||
38,
|
||||
254,
|
||||
159,
|
||||
231,
|
||||
165,
|
||||
1,
|
||||
94,
|
||||
64,
|
||||
137,
|
||||
243,
|
||||
76,
|
||||
249,
|
||||
101,
|
||||
251,
|
||||
129,
|
||||
33,
|
||||
101,
|
||||
189,
|
||||
30,
|
||||
42,
|
||||
11,
|
||||
191,
|
||||
34,
|
||||
103,
|
||||
186,
|
||||
227,
|
||||
230
|
||||
] ,
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
"supply_public_account": {
|
||||
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
|
||||
"balance": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"npk": [
|
||||
32,
|
||||
67,
|
||||
72,
|
||||
164,
|
||||
106,
|
||||
53,
|
||||
66,
|
||||
239,
|
||||
141,
|
||||
15,
|
||||
52,
|
||||
230,
|
||||
136,
|
||||
177,
|
||||
2,
|
||||
236,
|
||||
207,
|
||||
243,
|
||||
134,
|
||||
135,
|
||||
210,
|
||||
143,
|
||||
87,
|
||||
232,
|
||||
215,
|
||||
128,
|
||||
194,
|
||||
120,
|
||||
113,
|
||||
224,
|
||||
4,
|
||||
165
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
"supply_public_account": {
|
||||
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
|
||||
"balance": 20000
|
||||
}
|
||||
},
|
||||
{
|
||||
"supply_private_account": {
|
||||
"npk": [177,64,1,11,87,38,254,159,231,165,1,94,64,137,243,76,249,101,251,129,33,101,189,30,42,11,191,34,103,186,227,230],
|
||||
"balance": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"supply_private_account": {
|
||||
"npk": [32,67,72,164,106,53,66,239,141,15,52,230,136,177,2,236,207,243,134,135,210,143,87,232,215,128,194,120,113,224,4,165],
|
||||
"balance": 20000
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@ -22,10 +22,8 @@ ata_core.workspace = true
|
||||
indexer_service_rpc.workspace = true
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
wallet-ffi.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
|
||||
url.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@ -6,8 +6,7 @@ use indexer_service::{BackoffConfig, ChannelId, ClientConfig, IndexerConfig};
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use nssa::{Account, AccountId, PrivateKey, PublicKey};
|
||||
use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID};
|
||||
use sequencer_core::config::{BedrockConfig, SequencerConfig};
|
||||
use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData};
|
||||
use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig};
|
||||
use url::Url;
|
||||
use wallet::config::WalletConfig;
|
||||
|
||||
@ -100,27 +99,24 @@ impl InitialData {
|
||||
}
|
||||
}
|
||||
|
||||
fn sequencer_initial_public_accounts(&self) -> Vec<PublicAccountPublicInitialData> {
|
||||
fn sequencer_genesis(&self) -> Vec<GenesisTransaction> {
|
||||
self.public_accounts
|
||||
.iter()
|
||||
.map(|(priv_key, balance)| {
|
||||
let pub_key = PublicKey::new_from_private_key(priv_key);
|
||||
let account_id = AccountId::from(&pub_key);
|
||||
PublicAccountPublicInitialData {
|
||||
GenesisTransaction::SupplyPublicAccount {
|
||||
account_id,
|
||||
balance: *balance,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sequencer_initial_private_accounts(&self) -> Vec<PrivateAccountPublicInitialData> {
|
||||
self.private_accounts
|
||||
.iter()
|
||||
.map(|(key_chain, account)| PrivateAccountPublicInitialData {
|
||||
npk: key_chain.nullifier_public_key.clone(),
|
||||
account: account.clone(),
|
||||
})
|
||||
.chain(self.private_accounts.iter().map(|(key_chain, account)| {
|
||||
GenesisTransaction::SupplyPrivateAccount {
|
||||
npk: key_chain.nullifier_public_key.clone(),
|
||||
vpk: key_chain.viewing_public_key.clone(),
|
||||
balance: account.balance,
|
||||
}
|
||||
}))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@ -180,8 +176,7 @@ pub fn sequencer_config(
|
||||
mempool_max_size,
|
||||
block_create_timeout,
|
||||
retry_pending_blocks_timeout: Duration::from_secs(5),
|
||||
initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
|
||||
initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
|
||||
genesis: initial_data.sequencer_genesis(),
|
||||
signing_key: [37; 32],
|
||||
bedrock_config: BedrockConfig {
|
||||
backoff: BackoffConfig {
|
||||
|
||||
@ -13,7 +13,8 @@ use integration_tests::{
|
||||
};
|
||||
use key_protocol::key_management::{KeyChain, key_tree::chain_index::ChainIndex};
|
||||
use log::info;
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa::{AccountId, Data, program::Program};
|
||||
use nssa_core::account::Nonce;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
@ -349,8 +350,8 @@ async fn import_private_account() -> Result<()> {
|
||||
let account = nssa::Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: 777,
|
||||
data: Default::default(),
|
||||
nonce: Default::default(),
|
||||
data: Data::default(),
|
||||
nonce: Nonce::default(),
|
||||
};
|
||||
|
||||
let key_chain_json = serde_json::to_string(&key_chain)
|
||||
@ -394,8 +395,8 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<(
|
||||
let initial_account = nssa::Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: 100,
|
||||
data: Default::default(),
|
||||
nonce: Default::default(),
|
||||
data: Data::default(),
|
||||
nonce: Nonce::default(),
|
||||
};
|
||||
|
||||
// First import
|
||||
@ -411,8 +412,8 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<(
|
||||
let updated_account = nssa::Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: 999,
|
||||
data: Default::default(),
|
||||
nonce: Default::default(),
|
||||
data: Data::default(),
|
||||
nonce: Nonce::default(),
|
||||
};
|
||||
|
||||
// Second import with different account data (same key chain)
|
||||
|
||||
@ -18,7 +18,6 @@ common.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
log.workspace = true
|
||||
k256.workspace = true
|
||||
sha2.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
@ -517,10 +517,13 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionD
|
||||
/// - `pre_states`: The list of input accounts, each annotated with authorization metadata.
|
||||
/// - `post_states`: The list of resulting accounts after executing the program logic.
|
||||
/// - `executing_program_id`: The identifier of the program that was executed.
|
||||
/// - `is_genesis`: When `true`, skips the total balance conservation check (rule 8), allowing
|
||||
/// balance minting. Must only be `true` for genesis supply transactions.
|
||||
pub fn validate_execution(
|
||||
pre_states: &[AccountWithMetadata],
|
||||
post_states: &[AccountPostState],
|
||||
executing_program_id: ProgramId,
|
||||
is_genesis: bool,
|
||||
) -> Result<(), ExecutionValidationError> {
|
||||
// 1. Check account ids are all different
|
||||
if !validate_uniqueness_of_account_ids(pre_states) {
|
||||
@ -588,25 +591,26 @@ pub fn validate_execution(
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Total balance is preserved
|
||||
// 8. Total balance is preserved (skipped for genesis supply transactions)
|
||||
if !is_genesis {
|
||||
let Some(total_balance_pre_states) =
|
||||
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
|
||||
else {
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
let Some(total_balance_pre_states) =
|
||||
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
|
||||
else {
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
let Some(total_balance_post_states) =
|
||||
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
|
||||
else {
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
let Some(total_balance_post_states) =
|
||||
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
|
||||
else {
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
if total_balance_pre_states != total_balance_post_states {
|
||||
return Err(ExecutionValidationError::MismatchedTotalBalance {
|
||||
total_balance_pre_states,
|
||||
total_balance_post_states,
|
||||
});
|
||||
if total_balance_pre_states != total_balance_post_states {
|
||||
return Err(ExecutionValidationError::MismatchedTotalBalance {
|
||||
total_balance_pre_states,
|
||||
total_balance_post_states,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -10,8 +10,9 @@ use crate::{
|
||||
error::NssaError,
|
||||
program_methods::{
|
||||
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
|
||||
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
|
||||
PINATA_ID, TOKEN_ELF, TOKEN_ID,
|
||||
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID,
|
||||
GENESIS_SUPPLY_ACCOUNT_ELF, GENESIS_SUPPLY_ACCOUNT_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF,
|
||||
TOKEN_ID,
|
||||
},
|
||||
};
|
||||
|
||||
@ -148,6 +149,14 @@ impl Program {
|
||||
elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn genesis_supply_account() -> Self {
|
||||
Self {
|
||||
id: GENESIS_SUPPLY_ACCOUNT_ID,
|
||||
elf: GENESIS_SUPPLY_ACCOUNT_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
|
||||
|
||||
@ -177,7 +177,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -197,7 +197,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -218,7 +218,7 @@ pub mod tests {
|
||||
let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -254,7 +254,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,6 +176,8 @@ impl V03State {
|
||||
this.insert_program(Program::clock());
|
||||
this.insert_clock_accounts(genesis_timestamp);
|
||||
|
||||
this.insert_program(Program::genesis_supply_account());
|
||||
|
||||
this.insert_program(Program::authenticated_transfer_program());
|
||||
this.insert_program(Program::token());
|
||||
this.insert_program(Program::amm());
|
||||
@ -243,7 +245,8 @@ impl V03State {
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), NssaError> {
|
||||
let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?;
|
||||
let diff =
|
||||
ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp, false)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ impl ValidatedStateDiff {
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
is_genesis: bool,
|
||||
) -> Result<Self, NssaError> {
|
||||
let message = tx.message();
|
||||
let witness_set = tx.witness_set();
|
||||
@ -189,6 +190,7 @@ impl ValidatedStateDiff {
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
is_genesis,
|
||||
)
|
||||
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
|
||||
|
||||
@ -215,9 +217,10 @@ impl ValidatedStateDiff {
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
// The program can only claim accounts that were authorized by the signer if
|
||||
// it's not genesis.
|
||||
ensure!(
|
||||
is_authorized(&account_id),
|
||||
is_authorized(&account_id) || is_genesis,
|
||||
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,5 +16,6 @@ amm_core.workspace = true
|
||||
amm_program.workspace = true
|
||||
ata_core.workspace = true
|
||||
ata_program.workspace = true
|
||||
genesis_supply_account_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
serde = { workspace = true, default-features = false }
|
||||
|
||||
@ -8,7 +8,6 @@ use nssa_core::{
|
||||
/// Initializes a default account under the ownership of this program.
|
||||
fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState {
|
||||
let account_to_claim = AccountPostState::new_claimed(pre_state.account, Claim::Authorized);
|
||||
let is_authorized = pre_state.is_authorized;
|
||||
|
||||
// Continue only if the account to claim has default values
|
||||
assert!(
|
||||
@ -16,9 +15,6 @@ fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState {
|
||||
"Account must be uninitialized"
|
||||
);
|
||||
|
||||
// Continue only if the owner authorized this operation
|
||||
assert!(is_authorized, "Account must be authorized");
|
||||
|
||||
account_to_claim
|
||||
}
|
||||
|
||||
|
||||
129
program_methods/guest/src/bin/genesis_supply_account.rs
Normal file
129
program_methods/guest/src/bin/genesis_supply_account.rs
Normal file
@ -0,0 +1,129 @@
|
||||
//! Genesis Supply Account Program.
|
||||
//!
|
||||
//! A genesis-only program that supplies initial balance to an account.
|
||||
//! Uses the "initiate → callback" chained-call pattern to:
|
||||
//! 1. Initialize the target account via `authenticated_transfer`
|
||||
//! 2. Add the supplied balance in a self-callback
|
||||
//!
|
||||
//! This program verifies that the clock's `block_id` is 0, ensuring it can only
|
||||
//! execute at genesis. The balance increase violates normal balance conservation,
|
||||
//! so it requires `is_genesis: true` in the validation pipeline.
|
||||
//!
|
||||
//! # Accounts
|
||||
//!
|
||||
//! - `Initiate`: `[target_account, clock_account]`
|
||||
//! - `Callback`: `[target_account, clock_account]`
|
||||
|
||||
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
|
||||
use genesis_supply_account_core::Instruction;
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction,
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
match instruction {
|
||||
Instruction::Initiate {
|
||||
balance,
|
||||
authenticated_transfer_id,
|
||||
} => {
|
||||
let Ok([target_pre, clock_pre]) = <[_; 2]>::try_from(pre_states) else {
|
||||
panic!("Initiate requires exactly 2 accounts: target, clock");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Second account must be the clock account"
|
||||
);
|
||||
|
||||
let clock_data =
|
||||
ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner());
|
||||
assert_eq!(
|
||||
clock_data.block_id, 0,
|
||||
"Genesis supply can only execute at genesis (block_id must be 0)"
|
||||
);
|
||||
|
||||
// Compute the expected post-state of the target after authenticated_transfer
|
||||
// initializes it. authenticated_transfer claims the account (sets
|
||||
// program_owner = auth_transfer_id), leaving balance at 0.
|
||||
let mut target_after_init = target_pre.clone();
|
||||
target_after_init.account.program_owner = authenticated_transfer_id;
|
||||
|
||||
// Chained call 1: authenticated_transfer(0) — initializes (claims) the target account.
|
||||
let init_instruction =
|
||||
risc0_zkvm::serde::to_vec(&0_u128).expect("init instruction serialization");
|
||||
let call_1 = ChainedCall {
|
||||
program_id: authenticated_transfer_id,
|
||||
pre_states: vec![target_pre.clone()],
|
||||
instruction_data: init_instruction,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
// Chained call 2: self-callback to add the supplied balance.
|
||||
let callback_instruction =
|
||||
risc0_zkvm::serde::to_vec(&Instruction::Callback { balance })
|
||||
.expect("callback instruction serialization");
|
||||
let call_2 = ChainedCall {
|
||||
program_id: self_program_id,
|
||||
pre_states: vec![target_after_init, clock_pre.clone()],
|
||||
instruction_data: callback_instruction,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![target_pre.clone(), clock_pre.clone()],
|
||||
// post_states match pre_states — mutations happen in chained calls
|
||||
vec![
|
||||
AccountPostState::new(target_pre.account),
|
||||
AccountPostState::new(clock_pre.account),
|
||||
],
|
||||
)
|
||||
.with_chained_calls(vec![call_1, call_2])
|
||||
.write();
|
||||
}
|
||||
|
||||
Instruction::Callback { balance } => {
|
||||
// Access control: must be called from this program itself.
|
||||
assert_eq!(
|
||||
caller_program_id,
|
||||
Some(self_program_id),
|
||||
"Callback can only be invoked as a chained call from genesis_supply_account"
|
||||
);
|
||||
|
||||
let Ok([target_pre, clock_pre]) = <[_; 2]>::try_from(pre_states) else {
|
||||
panic!("Callback requires exactly 2 accounts: target, clock");
|
||||
};
|
||||
|
||||
// Add the supplied balance to the target account.
|
||||
let mut target_post_account = target_pre.account.clone();
|
||||
target_post_account.balance = target_post_account
|
||||
.balance
|
||||
.checked_add(balance)
|
||||
.expect("target balance overflow");
|
||||
|
||||
let target_post = AccountPostState::new(target_post_account);
|
||||
let clock_post = AccountPostState::new(clock_pre.account.clone());
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![target_pre, clock_pre],
|
||||
vec![target_post, clock_post],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ use std::{
|
||||
convert::Infallible,
|
||||
};
|
||||
|
||||
use clock_core::{CLOCK_PROGRAM_ACCOUNT_IDS, ClockAccountData};
|
||||
use nssa_core::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof,
|
||||
Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
|
||||
@ -125,10 +126,22 @@ impl ExecutionState {
|
||||
|
||||
// Check that the program is well behaved.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
//
|
||||
// TODO: This looks hacky as hell.
|
||||
// Derive is_genesis: true if any pre_state is a clock account at block 0.
|
||||
// This allows the genesis supply account program to mint balance at genesis.
|
||||
// Security: program execution is verified via env::verify(), and the genesis supply
|
||||
// program itself panics if clock.block_id != 0, so is_genesis cannot be forged.
|
||||
let is_genesis = program_output.pre_states.iter().any(|pre| {
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS.contains(&pre.account_id)
|
||||
&& ClockAccountData::from_bytes(&pre.account.data.clone().into_inner()).block_id
|
||||
== 0
|
||||
});
|
||||
let validated_execution = validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
is_genesis,
|
||||
);
|
||||
if let Err(err) = validated_execution {
|
||||
panic!(
|
||||
@ -151,6 +164,7 @@ impl ExecutionState {
|
||||
&authorized_pdas,
|
||||
program_output.pre_states,
|
||||
program_output.post_states,
|
||||
is_genesis,
|
||||
);
|
||||
chain_calls_counter = chain_calls_counter.checked_add(1).expect(
|
||||
"Chain calls counter should not overflow as it checked before incrementing",
|
||||
@ -194,6 +208,7 @@ impl ExecutionState {
|
||||
authorized_pdas: &HashSet<AccountId>,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
post_states: Vec<AccountPostState>,
|
||||
is_genesis: bool,
|
||||
) {
|
||||
for (pre, mut post) in pre_states.into_iter().zip(post_states) {
|
||||
let pre_account_id = pre.account_id;
|
||||
@ -264,7 +279,7 @@ impl ExecutionState {
|
||||
// Note: no need to check authorized pdas because we have already
|
||||
// checked consistency of authorization above.
|
||||
assert!(
|
||||
pre_is_authorized,
|
||||
pre_is_authorized || is_genesis,
|
||||
"Cannot claim unauthorized account {pre_account_id}"
|
||||
);
|
||||
}
|
||||
|
||||
12
programs/genesis_supply_account/core/Cargo.toml
Normal file
12
programs/genesis_supply_account/core/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "genesis_supply_account_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
serde.workspace = true
|
||||
25
programs/genesis_supply_account/core/src/lib.rs
Normal file
25
programs/genesis_supply_account/core/src/lib.rs
Normal file
@ -0,0 +1,25 @@
|
||||
//! Core data structures for the Genesis Supply Account Program.
|
||||
|
||||
use nssa_core::program::ProgramId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Instruction type for the Genesis Supply Account program.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// External entrypoint: initialize the target account then supply it with `balance`.
|
||||
///
|
||||
/// Required accounts: `[target_account, clock_account]`.
|
||||
///
|
||||
/// Emits 2 chained calls:
|
||||
/// 1. `authenticated_transfer(0)` on `[target_account]` — claims the account
|
||||
/// 2. `Callback { balance }` on `[target_after_init, clock_account]` — adds balance
|
||||
Initiate {
|
||||
balance: u128,
|
||||
authenticated_transfer_id: ProgramId,
|
||||
},
|
||||
/// Internal: add `balance` to the already-initialized target account.
|
||||
///
|
||||
/// Access control: only callable as a chained call from this program itself
|
||||
/// (enforced via `caller_program_id == Some(self_program_id)`).
|
||||
Callback { balance: u128 },
|
||||
}
|
||||
@ -15,6 +15,8 @@ storage.workspace = true
|
||||
mempool.workspace = true
|
||||
bedrock_client.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
genesis_supply_account_core.workspace = true
|
||||
key_protocol.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@ -6,6 +6,7 @@ use common::{
|
||||
block::{Block, BlockMeta, MantleMsgId},
|
||||
transaction::NSSATransaction,
|
||||
};
|
||||
use log::info;
|
||||
use nssa::V03State;
|
||||
use storage::{error::DbError, sequencer::RocksDBIO};
|
||||
|
||||
@ -18,21 +19,48 @@ pub struct SequencerStore {
|
||||
}
|
||||
|
||||
impl SequencerStore {
|
||||
/// Open existing database at the given location. Fails if no database is found.
|
||||
pub fn open_db(location: &Path, signing_key: nssa::PrivateKey) -> Result<Self> {
|
||||
let dbio = RocksDBIO::open(location)?;
|
||||
let genesis_id = dbio.get_meta_first_block_in_db()?;
|
||||
let last_id = dbio.latest_block_meta()?.id;
|
||||
|
||||
info!("Preparing block cache");
|
||||
let mut tx_hash_to_block_map = HashMap::new();
|
||||
for i in genesis_id..=last_id {
|
||||
let block = dbio
|
||||
.get_block(i)?
|
||||
.expect("Block should be present in the database");
|
||||
|
||||
tx_hash_to_block_map.extend(block_to_transactions_map(&block));
|
||||
}
|
||||
info!(
|
||||
"Block cache prepared. Total blocks in cache: {}",
|
||||
tx_hash_to_block_map.len()
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
dbio,
|
||||
tx_hash_to_block_map,
|
||||
genesis_id,
|
||||
signing_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Starting database at the start of new chain.
|
||||
/// Creates files if necessary.
|
||||
///
|
||||
/// ATTENTION: Will overwrite genesis block.
|
||||
pub fn open_db_with_genesis(
|
||||
pub fn create_db_with_genesis(
|
||||
location: &Path,
|
||||
genesis_block: &Block,
|
||||
genesis_msg_id: MantleMsgId,
|
||||
genesis_state: &V03State,
|
||||
signing_key: nssa::PrivateKey,
|
||||
) -> Result<Self> {
|
||||
let tx_hash_to_block_map = block_to_transactions_map(genesis_block);
|
||||
|
||||
let dbio = RocksDBIO::open_or_create(location, genesis_block, genesis_msg_id)?;
|
||||
|
||||
let dbio = RocksDBIO::create(location, genesis_block, genesis_msg_id, genesis_state)?;
|
||||
let genesis_id = dbio.get_meta_first_block_in_db()?;
|
||||
let tx_hash_to_block_map = block_to_transactions_map(genesis_block);
|
||||
|
||||
Ok(Self {
|
||||
dbio,
|
||||
@ -100,8 +128,8 @@ impl SequencerStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_nssa_state(&self) -> Option<V03State> {
|
||||
self.dbio.get_nssa_state().ok()
|
||||
pub fn get_nssa_state(&self) -> Result<V03State> {
|
||||
self.dbio.get_nssa_state().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,9 +167,14 @@ mod tests {
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
// Start an empty node store
|
||||
let mut node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
let mut node_store = SequencerStore::create_db_with_genesis(
|
||||
path,
|
||||
&genesis_block,
|
||||
[0; 32],
|
||||
&testnet_initial_state::initial_state(),
|
||||
signing_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||
@ -174,9 +207,14 @@ mod tests {
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
let genesis_hash = genesis_block.header.hash;
|
||||
|
||||
let node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
let node_store = SequencerStore::create_db_with_genesis(
|
||||
path,
|
||||
&genesis_block,
|
||||
[0; 32],
|
||||
&testnet_initial_state::initial_state(),
|
||||
signing_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Verify that initially the latest block hash equals genesis hash
|
||||
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||
@ -199,9 +237,14 @@ mod tests {
|
||||
};
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
let mut node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
let mut node_store = SequencerStore::create_db_with_genesis(
|
||||
path,
|
||||
&genesis_block,
|
||||
[0; 32],
|
||||
&testnet_initial_state::initial_state(),
|
||||
signing_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Add a new block
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
@ -235,9 +278,14 @@ mod tests {
|
||||
};
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
let mut node_store =
|
||||
SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key)
|
||||
.unwrap();
|
||||
let mut node_store = SequencerStore::create_db_with_genesis(
|
||||
path,
|
||||
&genesis_block,
|
||||
[0; 32],
|
||||
&testnet_initial_state::initial_state(),
|
||||
signing_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Add a new block with Pending status
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
@ -264,4 +312,49 @@ mod tests {
|
||||
common::block::BedrockStatus::Finalized
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_existing_db_caches_transactions() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
let signing_key = sequencer_sign_key_for_testing();
|
||||
|
||||
let genesis_block_hashable_data = HashableBlockData {
|
||||
block_id: 0,
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
transactions: vec![],
|
||||
};
|
||||
|
||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
{
|
||||
// Create a scope to drop the first store after creating the db
|
||||
let mut node_store = SequencerStore::create_db_with_genesis(
|
||||
path,
|
||||
&genesis_block,
|
||||
[0; 32],
|
||||
&testnet_initial_state::initial_state(),
|
||||
signing_key.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Add a new block
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||
node_store
|
||||
.update(
|
||||
&block,
|
||||
[1; 32],
|
||||
&V03State::new_with_genesis_accounts(&[], vec![], 0),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Re-open the store and verify that the transaction is still retrievable (which means it
|
||||
// was cached correctly)
|
||||
let node_store = SequencerStore::open_db(path, signing_key).unwrap();
|
||||
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
||||
assert_eq!(Some(tx), retrieved_tx);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,26 @@ use bytesize::ByteSize;
|
||||
use common::config::BasicAuth;
|
||||
use humantime_serde;
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use nssa::AccountId;
|
||||
use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData};
|
||||
use url::Url;
|
||||
|
||||
/// A transaction to be applied at genesis to supply initial balances.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenesisTransaction {
|
||||
SupplyPublicAccount {
|
||||
account_id: AccountId,
|
||||
balance: u128,
|
||||
},
|
||||
SupplyPrivateAccount {
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
balance: u128,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Provide default values
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SequencerConfig {
|
||||
@ -44,10 +60,9 @@ pub struct SequencerConfig {
|
||||
pub bedrock_config: BedrockConfig,
|
||||
/// Indexer RPC URL.
|
||||
pub indexer_rpc_url: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initial_public_accounts: Option<Vec<PublicAccountPublicInitialData>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initial_private_accounts: Option<Vec<PrivateAccountPublicInitialData>>,
|
||||
/// Genesis configuration.
|
||||
#[serde(default)]
|
||||
pub genesis: Vec<GenesisTransaction>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
|
||||
@ -1,23 +1,25 @@
|
||||
use std::{path::Path, time::Instant};
|
||||
use std::{collections::HashMap, path::Path, time::Instant};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use bedrock_client::SignedMantleTx;
|
||||
#[cfg(feature = "testnet")]
|
||||
use common::PINATA_BASE58;
|
||||
use common::{
|
||||
HashType,
|
||||
block::{BedrockStatus, Block, HashableBlockData},
|
||||
transaction::{NSSATransaction, clock_invocation},
|
||||
};
|
||||
use config::SequencerConfig;
|
||||
use config::{GenesisTransaction, SequencerConfig};
|
||||
use log::{error, info, warn};
|
||||
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
|
||||
use mempool::{MemPool, MemPoolHandle};
|
||||
#[cfg(feature = "mock")]
|
||||
pub use mock::SequencerCoreWithMockClients;
|
||||
use nssa::V03State;
|
||||
use nssa::{
|
||||
Account, AccountId, PrivacyPreservingTransaction, PublicTransaction, ValidatedStateDiff,
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
public_transaction::Message,
|
||||
};
|
||||
use nssa_core::account::AccountWithMetadata;
|
||||
pub use storage::error::DbError;
|
||||
use testnet_initial_state::initial_state;
|
||||
|
||||
use crate::{
|
||||
block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait, MsgId},
|
||||
@ -55,16 +57,8 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
pub async fn start_from_config(
|
||||
config: SequencerConfig,
|
||||
) -> (Self, MemPoolHandle<NSSATransaction>) {
|
||||
let hashable_data = HashableBlockData {
|
||||
block_id: config.genesis_id,
|
||||
transactions: vec![],
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap();
|
||||
let genesis_parent_msg_id = [0; 32];
|
||||
let genesis_block = hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id);
|
||||
let db_path = config.home.join("rocksdb");
|
||||
|
||||
let bedrock_signing_key =
|
||||
load_or_create_signing_key(&config.home.join("bedrock_signing_key"))
|
||||
@ -77,79 +71,52 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
.await
|
||||
.expect("Failed to create Indexer Client");
|
||||
|
||||
let (_tx, genesis_msg_id) = block_settlement_client
|
||||
.create_inscribe_tx(&genesis_block)
|
||||
.expect("Failed to create inscribe tx for genesis block");
|
||||
let (store, state) = SequencerStore::open_db(&db_path, signing_key.clone()).map_or_else(
|
||||
|err| {
|
||||
warn!(
|
||||
"Failed to open existing database at {} with error: {err:#?}. Starting from genesis.",
|
||||
db_path.display()
|
||||
);
|
||||
|
||||
let genesis_parent_msg_id = [0; 32];
|
||||
let (genesis_state, genesis_txs) = build_genesis_state(&config);
|
||||
|
||||
let hashable_data = HashableBlockData {
|
||||
block_id: config.genesis_id,
|
||||
transactions: genesis_txs,
|
||||
prev_block_hash: HashType([0; 32]),
|
||||
timestamp: 0,
|
||||
};
|
||||
let genesis_block =
|
||||
hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id);
|
||||
|
||||
let (_tx, genesis_msg_id) = block_settlement_client
|
||||
.create_inscribe_tx(&genesis_block)
|
||||
.expect("Failed to create inscribe tx for genesis block");
|
||||
|
||||
let store = SequencerStore::create_db_with_genesis(
|
||||
&db_path,
|
||||
&genesis_block,
|
||||
genesis_msg_id.into(),
|
||||
&genesis_state,
|
||||
signing_key,
|
||||
)
|
||||
.expect("Failed to create database with genesis block");
|
||||
|
||||
(store, genesis_state)
|
||||
},
|
||||
|store| {
|
||||
let state = store
|
||||
.get_nssa_state()
|
||||
.expect("Failed to read state from store");
|
||||
(store, state)
|
||||
}
|
||||
);
|
||||
|
||||
// Sequencer should panic if unable to open db,
|
||||
// as fixing this issue may require actions non-native to program scope
|
||||
let store = SequencerStore::open_db_with_genesis(
|
||||
&config.home.join("rocksdb"),
|
||||
&genesis_block,
|
||||
genesis_msg_id.into(),
|
||||
signing_key,
|
||||
)
|
||||
.unwrap();
|
||||
let latest_block_meta = store
|
||||
.latest_block_meta()
|
||||
.expect("Failed to read latest block meta from store");
|
||||
|
||||
#[cfg_attr(not(feature = "testnet"), allow(unused_mut))]
|
||||
let mut state = if let Some(state) = store.get_nssa_state() {
|
||||
info!("Found local database. Loading state and pending blocks from it.");
|
||||
state
|
||||
} else {
|
||||
info!(
|
||||
"No database found when starting the sequencer. Creating a fresh new with the initial data"
|
||||
);
|
||||
|
||||
let initial_private_accounts: Option<
|
||||
Vec<(nssa_core::Commitment, nssa_core::Nullifier)>,
|
||||
> = config.initial_private_accounts.clone().map(|accounts| {
|
||||
accounts
|
||||
.iter()
|
||||
.map(|init_comm_data| {
|
||||
let npk = &init_comm_data.npk;
|
||||
|
||||
let mut acc = init_comm_data.account.clone();
|
||||
|
||||
acc.program_owner =
|
||||
nssa::program::Program::authenticated_transfer_program().id();
|
||||
|
||||
(
|
||||
nssa_core::Commitment::new(npk, &acc),
|
||||
nssa_core::Nullifier::for_account_initialization(npk),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
let init_accs: Option<Vec<(nssa::AccountId, u128)>> = config
|
||||
.initial_public_accounts
|
||||
.clone()
|
||||
.map(|initial_accounts| {
|
||||
initial_accounts
|
||||
.iter()
|
||||
.map(|acc_data| (acc_data.account_id, acc_data.balance))
|
||||
.collect()
|
||||
});
|
||||
|
||||
// If initial commitments or accounts are present in config, need to construct state
|
||||
// from them
|
||||
if initial_private_accounts.is_some() || init_accs.is_some() {
|
||||
V03State::new_with_genesis_accounts(
|
||||
&init_accs.unwrap_or_default(),
|
||||
initial_private_accounts.unwrap_or_default(),
|
||||
genesis_block.header.timestamp,
|
||||
)
|
||||
} else {
|
||||
initial_state()
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "testnet")]
|
||||
state.add_pinata_program(PINATA_BASE58.parse().unwrap());
|
||||
|
||||
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
||||
|
||||
let sequencer_core = Self {
|
||||
@ -362,6 +329,134 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the initial genesis state from `testnet_initial_state` plus configured genesis
|
||||
/// transactions. Returns the final state and the list of [`NSSATransaction`]s that should be
|
||||
/// committed to the genesis block so external observers can replay them.
|
||||
fn build_genesis_state(config: &SequencerConfig) -> (nssa::V03State, Vec<NSSATransaction>) {
|
||||
#[cfg(not(feature = "testnet"))]
|
||||
let mut state = testnet_initial_state::initial_state();
|
||||
|
||||
#[cfg(feature = "testnet")]
|
||||
let mut state = testnet_initial_state::initial_state_testnet();
|
||||
|
||||
let genesis_txs = config
|
||||
.genesis
|
||||
.iter()
|
||||
.map(|genesis_tx| {
|
||||
let (tx, diff) = match genesis_tx {
|
||||
GenesisTransaction::SupplyPublicAccount {
|
||||
account_id,
|
||||
balance,
|
||||
} => build_public_genesis_transaction(config, &state, account_id, *balance),
|
||||
GenesisTransaction::SupplyPrivateAccount { npk, vpk, balance } => {
|
||||
build_private_genesis_transaction(
|
||||
config,
|
||||
&state,
|
||||
npk.clone(),
|
||||
vpk.clone(),
|
||||
*balance,
|
||||
)
|
||||
}
|
||||
};
|
||||
state.apply_state_diff(diff);
|
||||
tx
|
||||
})
|
||||
.collect();
|
||||
|
||||
(state, genesis_txs)
|
||||
}
|
||||
|
||||
fn build_public_genesis_transaction(
|
||||
config: &SequencerConfig,
|
||||
state: &nssa::V03State,
|
||||
account_id: &AccountId,
|
||||
balance: u128,
|
||||
) -> (NSSATransaction, ValidatedStateDiff) {
|
||||
let authenticated_transfer_id = Program::authenticated_transfer_program().id();
|
||||
let genesis_supply_id = nssa::program::Program::genesis_supply_account().id();
|
||||
|
||||
let message = Message::try_new(
|
||||
genesis_supply_id,
|
||||
vec![*account_id, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID],
|
||||
vec![],
|
||||
genesis_supply_account_core::Instruction::Initiate {
|
||||
balance,
|
||||
authenticated_transfer_id,
|
||||
},
|
||||
)
|
||||
.expect("Failed to serialize genesis supply instruction");
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, state, config.genesis_id, 0, true)
|
||||
.expect("Failed to execute genesis supply public transaction");
|
||||
|
||||
(tx.into(), diff)
|
||||
}
|
||||
|
||||
fn build_private_genesis_transaction(
|
||||
config: &SequencerConfig,
|
||||
state: &nssa::V03State,
|
||||
npk: nssa_core::NullifierPublicKey,
|
||||
vpk: nssa_core::encryption::ViewingPublicKey,
|
||||
balance: u128,
|
||||
) -> (NSSATransaction, ValidatedStateDiff) {
|
||||
let authenticated_transfer = Program::authenticated_transfer_program();
|
||||
let account_id = AccountId::from(&npk);
|
||||
let clock_account = state.get_account_by_id(nssa::CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
let pre_states = vec![
|
||||
AccountWithMetadata::new(Account::default(), false, account_id),
|
||||
AccountWithMetadata::new(clock_account, false, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID),
|
||||
];
|
||||
let instruction = nssa::program::Program::serialize_instruction(
|
||||
genesis_supply_account_core::Instruction::Initiate {
|
||||
balance,
|
||||
authenticated_transfer_id: authenticated_transfer.id(),
|
||||
},
|
||||
)
|
||||
.expect("Failed to serialize genesis supply private account instruction");
|
||||
|
||||
let eph_holder =
|
||||
key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder::new(&npk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender(&vpk);
|
||||
let epk = eph_holder.generate_ephemeral_public_key();
|
||||
|
||||
let genesis_supply_account = Program::genesis_supply_account();
|
||||
let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
pre_states,
|
||||
instruction,
|
||||
vec![2, 0],
|
||||
vec![(npk.clone(), ssk)],
|
||||
vec![],
|
||||
vec![None],
|
||||
&ProgramWithDependencies::new(
|
||||
genesis_supply_account.clone(),
|
||||
HashMap::from([
|
||||
(authenticated_transfer.id(), authenticated_transfer),
|
||||
(genesis_supply_account.id(), genesis_supply_account),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.expect("Failed to execute and prove genesis supply private account transaction");
|
||||
|
||||
let message = nssa::privacy_preserving_transaction::Message::try_from_circuit_output(
|
||||
vec![nssa::CLOCK_01_PROGRAM_ACCOUNT_ID],
|
||||
vec![],
|
||||
vec![(npk, vpk, epk)],
|
||||
output,
|
||||
)
|
||||
.expect("Failed to serialize genesis supply private account instruction");
|
||||
|
||||
let witness_set =
|
||||
nssa::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
let diff =
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, state, config.genesis_id, 0)
|
||||
.expect("Failed to validate genesis supply private account transaction");
|
||||
(tx.into(), diff)
|
||||
}
|
||||
|
||||
/// Load signing key from file or generate a new one if it doesn't exist.
|
||||
fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
|
||||
if path.exists() {
|
||||
@ -401,7 +496,7 @@ mod tests {
|
||||
use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys};
|
||||
|
||||
use crate::{
|
||||
config::{BedrockConfig, SequencerConfig},
|
||||
config::{BedrockConfig, GenesisTransaction, SequencerConfig},
|
||||
mock::SequencerCoreWithMockClients,
|
||||
};
|
||||
|
||||
@ -429,8 +524,7 @@ mod tests {
|
||||
},
|
||||
retry_pending_blocks_timeout: Duration::from_mins(4),
|
||||
indexer_rpc_url: "ws://localhost:8779".parse().unwrap(),
|
||||
initial_public_accounts: None,
|
||||
initial_private_accounts: None,
|
||||
genesis: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1079,24 +1173,21 @@ mod tests {
|
||||
account::AccountWithMetadata,
|
||||
encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey},
|
||||
};
|
||||
use testnet_initial_state::PrivateAccountPublicInitialData;
|
||||
|
||||
let nsk: nssa_core::NullifierSecretKey = [7; 32];
|
||||
let npk = nssa_core::NullifierPublicKey::from(&nsk);
|
||||
let vsk: EphemeralSecretKey = [8; 32];
|
||||
let vpk = ViewingPublicKey::from_scalar(vsk);
|
||||
|
||||
let genesis_account = Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
// Start a sequencer from config with a preconfigured private genesis account
|
||||
let mut config = setup_sequencer_config();
|
||||
config.initial_private_accounts = Some(vec![PrivateAccountPublicInitialData {
|
||||
npk: npk.clone(),
|
||||
account: genesis_account,
|
||||
}]);
|
||||
config
|
||||
.genesis
|
||||
.push(GenesisTransaction::SupplyPrivateAccount {
|
||||
npk: npk.clone(),
|
||||
vpk: vpk.clone(),
|
||||
balance: 10,
|
||||
});
|
||||
|
||||
let (mut sequencer, _mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config).await;
|
||||
|
||||
@ -40,36 +40,26 @@ impl DBIO for RocksDBIO {
|
||||
}
|
||||
|
||||
impl RocksDBIO {
|
||||
pub fn open_or_create(
|
||||
pub fn open(path: &Path) -> DbResult<Self> {
|
||||
let db_opts = Options::default();
|
||||
Self::open_inner(path, &db_opts)
|
||||
}
|
||||
|
||||
pub fn create(
|
||||
path: &Path,
|
||||
genesis_block: &Block,
|
||||
genesis_msg_id: MantleMsgId,
|
||||
genesis_state: &V03State,
|
||||
) -> DbResult<Self> {
|
||||
let mut cf_opts = Options::default();
|
||||
cf_opts.set_max_write_buffer_number(16);
|
||||
// ToDo: Add more column families for different data
|
||||
let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone());
|
||||
let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone());
|
||||
let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone());
|
||||
|
||||
let mut db_opts = Options::default();
|
||||
db_opts.create_missing_column_families(true);
|
||||
db_opts.create_if_missing(true);
|
||||
let db = DBWithThreadMode::<MultiThreaded>::open_cf_descriptors(
|
||||
&db_opts,
|
||||
path,
|
||||
vec![cfb, cfmeta, cfstate],
|
||||
)
|
||||
.map_err(|err| DbError::RocksDbError {
|
||||
error: err,
|
||||
additional_info: Some("Failed to open or create DB".to_owned()),
|
||||
})?;
|
||||
|
||||
let dbio = Self { db };
|
||||
let dbio = Self::open_inner(path, &db_opts)?;
|
||||
|
||||
let is_start_set = dbio.get_meta_is_first_block_set()?;
|
||||
if !is_start_set {
|
||||
let block_id = genesis_block.header.block_id;
|
||||
// TODO: Shouldn't this be atomic (batched)?
|
||||
dbio.put_meta_first_block_in_db(genesis_block, genesis_msg_id)?;
|
||||
dbio.put_meta_is_first_block_set()?;
|
||||
dbio.put_meta_last_block_in_db(block_id)?;
|
||||
@ -79,11 +69,35 @@ impl RocksDBIO {
|
||||
hash: genesis_block.header.hash,
|
||||
msg_id: genesis_msg_id,
|
||||
})?;
|
||||
dbio.put_nssa_state_in_db(genesis_state)?;
|
||||
}
|
||||
|
||||
Ok(dbio)
|
||||
}
|
||||
|
||||
fn open_inner(path: &Path, db_opts: &Options) -> DbResult<Self> {
|
||||
let mut cf_opts = Options::default();
|
||||
cf_opts.set_max_write_buffer_number(16);
|
||||
|
||||
// ToDo: Add more column families for different data
|
||||
let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone());
|
||||
let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone());
|
||||
let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone());
|
||||
|
||||
let db = DBWithThreadMode::<MultiThreaded>::open_cf_descriptors(
|
||||
db_opts,
|
||||
path,
|
||||
vec![cfb, cfmeta, cfstate],
|
||||
)
|
||||
.map_err(|err| DbError::RocksDbError {
|
||||
error: err,
|
||||
additional_info: Some("Failed to open or create DB".to_owned()),
|
||||
})?;
|
||||
|
||||
let dbio = Self { db };
|
||||
Ok(dbio)
|
||||
}
|
||||
|
||||
pub fn destroy(path: &Path) -> DbResult<()> {
|
||||
let mut cf_opts = Options::default();
|
||||
cf_opts.set_max_write_buffer_number(16);
|
||||
@ -133,7 +147,15 @@ impl RocksDBIO {
|
||||
Ok(self.get_opt::<FirstBlockSetCell>(())?.is_some())
|
||||
}
|
||||
|
||||
pub fn put_nssa_state_in_db(&self, state: &V03State, batch: &mut WriteBatch) -> DbResult<()> {
|
||||
pub fn put_nssa_state_in_db(&self, state: &V03State) -> DbResult<()> {
|
||||
self.put(&NSSAStateCellRef(state), ())
|
||||
}
|
||||
|
||||
pub fn put_nssa_state_in_db_batch(
|
||||
&self,
|
||||
state: &V03State,
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<()> {
|
||||
self.put_batch(&NSSAStateCellRef(state), (), batch)
|
||||
}
|
||||
|
||||
@ -338,7 +360,7 @@ impl RocksDBIO {
|
||||
let block_id = block.header.block_id;
|
||||
let mut batch = WriteBatch::default();
|
||||
self.put_block(block, msg_id, false, &mut batch)?;
|
||||
self.put_nssa_state_in_db(state, &mut batch)?;
|
||||
self.put_nssa_state_in_db_batch(state, &mut batch)?;
|
||||
self.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user