mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-03 05:29:50 +00:00
Merge 51e4141a93a41f5b9672596e68ee98685deb0887 into fe4c7a96da393808946d0ffdb9ef44a5da9d8ef0
This commit is contained in:
commit
a808b649ff
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@ -125,6 +125,38 @@ jobs:
|
||||
env:
|
||||
RISC0_DEV_MODE: 1
|
||||
|
||||
local-sequencer-integration-tests:
|
||||
name: Local Sequencer Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.94.0"
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cache/logos-scaffold
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-scaffold-test-node-${{ hashFiles('**/Cargo.lock', 'scaffold.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-scaffold-test-node-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Local sequencer integration tests
|
||||
run: cargo test -p integration_tests --features local-sequencer-tests -- --nocapture
|
||||
env:
|
||||
RISC0_BUILD_DEBUG: 0
|
||||
RISC0_DEV_MODE: 1
|
||||
RUST_TEST_THREADS: 1
|
||||
|
||||
build-programs:
|
||||
name: Build Guest Programs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
target/
|
||||
.scaffold/
|
||||
*.bin
|
||||
**/*/result
|
||||
|
||||
679
Cargo.lock
generated
679
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -67,10 +67,17 @@ RISC0_DEV_MODE=1 cargo test -p token_program -p amm_program -p ata_program -p st
|
||||
# Run integration tests (dev mode skips ZK proof generation)
|
||||
RISC0_DEV_MODE=1 cargo test -p integration_tests
|
||||
|
||||
# Run integration tests through scaffold-managed local sequencer test nodes.
|
||||
# RISC0_BUILD_DEBUG=0 keeps embedded guest ELFs on the release profile while
|
||||
# RISC0_DEV_MODE=1 skips proof generation.
|
||||
RISC0_BUILD_DEBUG=0 RISC0_DEV_MODE=1 RUST_TEST_THREADS=1 cargo test -p integration_tests --features local-sequencer-tests -- --nocapture
|
||||
|
||||
# Run all tests
|
||||
make test
|
||||
```
|
||||
|
||||
The `local-sequencer-tests` feature mirrors the suite through scaffold-managed test nodes configured by `scaffold.toml`.
|
||||
|
||||
Integration tests live in `programs/integration_tests/tests/` and cover `token`, `amm`, and `ata` programs end-to-end through the zkVM using `RISC0_DEV_MODE=1` to skip proof generation. Each test file corresponds to a program:
|
||||
|
||||
- `programs/integration_tests/tests/token.rs`
|
||||
|
||||
@ -6,10 +6,22 @@ edition = "2021"
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
local-sequencer-tests = [
|
||||
"dep:base64",
|
||||
"dep:borsh",
|
||||
"dep:logos-scaffold",
|
||||
"dep:rocksdb",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
nssa = { workspace = true }
|
||||
nssa_core = { workspace = true, features = ["host", "test_utils"] }
|
||||
clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc6" }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
borsh = { workspace = true, optional = true }
|
||||
logos-scaffold = { git = "https://github.com/logos-co/scaffold.git", rev = "9e4d148d08fd86213d3becc17d6863895048e9cb", package = "logos-scaffold", optional = true }
|
||||
rocksdb = { version = "0.24.0", default-features = false, features = ["snappy", "bindgen-runtime"], optional = true }
|
||||
amm_core = { workspace = true }
|
||||
token_core = { workspace = true }
|
||||
ata_core = { workspace = true }
|
||||
|
||||
@ -1 +1,8 @@
|
||||
#[cfg(feature = "local-sequencer-tests")]
|
||||
mod local_sequencer;
|
||||
|
||||
#[cfg(feature = "local-sequencer-tests")]
|
||||
pub use local_sequencer::TestState;
|
||||
|
||||
#[cfg(not(feature = "local-sequencer-tests"))]
|
||||
pub type TestState = nssa::V03State;
|
||||
|
||||
635
programs/integration_tests/src/local_sequencer.rs
Normal file
635
programs/integration_tests/src/local_sequencer.rs
Normal file
@ -0,0 +1,635 @@
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
fmt::Write as _,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
sync::OnceLock,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
use borsh::BorshSerialize;
|
||||
use logos_scaffold::api::{
|
||||
testnode::{
|
||||
AccountValue, PinOverrides, ProofValue, ReadAt, RejectionPhase, TestNode, TestNodeClient,
|
||||
TestNodeConfig, TransactionBytes, TransactionOutcome, WaitOptions,
|
||||
},
|
||||
Project,
|
||||
};
|
||||
use nssa::{
|
||||
error::LeeError, privacy_preserving_transaction::PrivacyPreservingTransaction,
|
||||
program_deployment_transaction::ProgramDeploymentTransaction,
|
||||
public_transaction::PublicTransaction,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId},
|
||||
Commitment, MembershipProof, Nullifier, Timestamp,
|
||||
};
|
||||
use rocksdb::{ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options};
|
||||
|
||||
type DynError = Box<dyn Error + Send + Sync>;
|
||||
type DynResult<T> = Result<T, DynError>;
|
||||
|
||||
const TEST_NODE_CIRCUITS_VERSION_ENV: &str = "LOGOS_SCAFFOLD_TEST_NODE_CIRCUITS_VERSION";
|
||||
const RISC0_BUILD_DEBUG_ENV: &str = "RISC0_BUILD_DEBUG";
|
||||
const DEFAULT_CIRCUITS_VERSION: &str = "0.4.2";
|
||||
const CF_BLOCK_NAME: &str = "cf_block";
|
||||
const CF_META_NAME: &str = "cf_meta";
|
||||
const CF_NSSA_STATE_NAME: &str = "cf_nssa_state";
|
||||
const DB_NSSA_STATE_KEY: &str = "nssa_state";
|
||||
const POLL_INTERVAL: Duration = Duration::from_millis(100);
|
||||
const COMMIT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
const HEALTH_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
pub struct TestState {
|
||||
inner: nssa::V03State,
|
||||
sequencer: Option<LocalSequencer>,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
impl TestState {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: nssa::V03State::new(),
|
||||
sequencer: None,
|
||||
dirty: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_with_genesis_accounts(
|
||||
public_accounts: &[(AccountId, u128)],
|
||||
private_accounts: Vec<(Commitment, Nullifier)>,
|
||||
_genesis_timestamp: Timestamp,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: nssa::V03State::new()
|
||||
.with_public_account_balances(public_accounts.iter().copied())
|
||||
.with_private_accounts(private_accounts),
|
||||
sequencer: None,
|
||||
dirty: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_from_public_transaction(
|
||||
&mut self,
|
||||
tx: &PublicTransaction,
|
||||
block_id: u64,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), LeeError> {
|
||||
let rpc_tx = RpcTransaction::Public(Box::new(tx.clone()));
|
||||
match self.mirror_transaction(&rpc_tx) {
|
||||
MirrorOutcome::Committed(context) => {
|
||||
let mut next = self.inner.clone();
|
||||
next.transition_from_public_transaction(tx, context.block_id, context.timestamp)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"local replay rejected public transaction committed by sequencer at \
|
||||
block {}: {err}",
|
||||
context.block_id
|
||||
)
|
||||
});
|
||||
self.inner = next;
|
||||
self.assert_affected_accounts_match(&rpc_tx);
|
||||
Ok(())
|
||||
}
|
||||
MirrorOutcome::NotCommitted(rejection) => {
|
||||
let mut expected = self.inner.clone();
|
||||
let result = if let Some(context) = rejection.validation_context {
|
||||
expected.transition_from_public_transaction(
|
||||
tx,
|
||||
context.block_id,
|
||||
context.timestamp,
|
||||
)
|
||||
} else {
|
||||
expected.transition_from_public_transaction(tx, block_id, timestamp)
|
||||
};
|
||||
|
||||
if result.is_ok() {
|
||||
panic!("local replay accepted public transaction dropped by sequencer");
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_from_privacy_preserving_transaction(
|
||||
&mut self,
|
||||
tx: &PrivacyPreservingTransaction,
|
||||
block_id: u64,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), LeeError> {
|
||||
let rpc_tx = RpcTransaction::PrivacyPreserving(Box::new(tx.clone()));
|
||||
match self.mirror_transaction(&rpc_tx) {
|
||||
MirrorOutcome::Committed(context) => {
|
||||
let mut next = self.inner.clone();
|
||||
next.transition_from_privacy_preserving_transaction(
|
||||
tx,
|
||||
context.block_id,
|
||||
context.timestamp,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"local replay rejected privacy-preserving transaction committed by \
|
||||
sequencer at block {}: {err}",
|
||||
context.block_id
|
||||
)
|
||||
});
|
||||
self.inner = next;
|
||||
self.assert_affected_accounts_match(&rpc_tx);
|
||||
Ok(())
|
||||
}
|
||||
MirrorOutcome::NotCommitted(rejection) => {
|
||||
let mut expected = self.inner.clone();
|
||||
let result = if let Some(context) = rejection.validation_context {
|
||||
expected.transition_from_privacy_preserving_transaction(
|
||||
tx,
|
||||
context.block_id,
|
||||
context.timestamp,
|
||||
)
|
||||
} else {
|
||||
expected.transition_from_privacy_preserving_transaction(tx, block_id, timestamp)
|
||||
};
|
||||
|
||||
if result.is_ok() {
|
||||
panic!(
|
||||
"local replay accepted privacy-preserving transaction dropped by sequencer"
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_from_program_deployment_transaction(
|
||||
&mut self,
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
) -> Result<(), LeeError> {
|
||||
let rpc_tx = RpcTransaction::ProgramDeployment(Box::new(tx.clone()));
|
||||
match self.mirror_transaction(&rpc_tx) {
|
||||
MirrorOutcome::Committed(context) => {
|
||||
let mut next = self.inner.clone();
|
||||
next.transition_from_program_deployment_transaction(tx)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"local replay rejected program deployment committed by sequencer at \
|
||||
block {}: {err}",
|
||||
context.block_id
|
||||
)
|
||||
});
|
||||
self.inner = next;
|
||||
self.assert_affected_accounts_match(&rpc_tx);
|
||||
Ok(())
|
||||
}
|
||||
MirrorOutcome::NotCommitted(_) => {
|
||||
let mut expected = self.inner.clone();
|
||||
let result = expected.transition_from_program_deployment_transaction(tx);
|
||||
if result.is_ok() {
|
||||
panic!("local replay accepted program deployment dropped by sequencer");
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn force_insert_account(&mut self, account_id: AccountId, account: Account) {
|
||||
self.inner.force_insert_account(account_id, account);
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_account_by_id(&self, account_id: AccountId) -> Account {
|
||||
let account = self.inner.get_account_by_id(account_id);
|
||||
if !self.dirty {
|
||||
if let Some(sequencer) = &self.sequencer {
|
||||
let sequencer_account = sequencer
|
||||
.get_account_by_id(account_id)
|
||||
.unwrap_or_else(|err| panic!("local sequencer getAccount failed: {err}"));
|
||||
assert_eq!(
|
||||
sequencer_account, account,
|
||||
"local sequencer account state diverged for {account_id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
account
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option<MembershipProof> {
|
||||
let proof = self.inner.get_proof_for_commitment(commitment);
|
||||
if !self.dirty {
|
||||
if let Some(sequencer) = &self.sequencer {
|
||||
let sequencer_proof = sequencer
|
||||
.get_proof_for_commitment(commitment)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("local sequencer getProofForCommitment failed: {err}")
|
||||
});
|
||||
assert_eq!(
|
||||
sequencer_proof, proof,
|
||||
"local sequencer commitment proof diverged"
|
||||
);
|
||||
}
|
||||
}
|
||||
proof
|
||||
}
|
||||
|
||||
fn mirror_transaction(&mut self, tx: &RpcTransaction) -> MirrorOutcome {
|
||||
let tx_hash = hex_encode(&tx.hash());
|
||||
let outcome = self
|
||||
.ensure_sequencer()
|
||||
.submit_and_wait(tx, COMMIT_TIMEOUT)
|
||||
.unwrap_or_else(|err| panic!("local sequencer failed to submit {tx_hash}: {err}"));
|
||||
|
||||
match outcome {
|
||||
TransactionOutcome::Committed { block, .. } => {
|
||||
MirrorOutcome::Committed(ObservedBlock {
|
||||
block_id: block.block_id,
|
||||
timestamp: block.timestamp,
|
||||
})
|
||||
}
|
||||
TransactionOutcome::Rejected {
|
||||
phase: RejectionPhase::Stateless,
|
||||
..
|
||||
} => MirrorOutcome::NotCommitted(RejectionContext::precheck()),
|
||||
TransactionOutcome::Rejected {
|
||||
phase: RejectionPhase::Stateful,
|
||||
observed_after_block_id,
|
||||
..
|
||||
} => {
|
||||
let validation_context = observed_after_block_id
|
||||
.and_then(|block_id| self.sequencer.as_ref()?.block_context(block_id).ok());
|
||||
MirrorOutcome::NotCommitted(RejectionContext { validation_context })
|
||||
}
|
||||
TransactionOutcome::Timeout {
|
||||
last_observed_block_id,
|
||||
..
|
||||
} => {
|
||||
panic!(
|
||||
"local sequencer timed out waiting for {tx_hash}; last observed block \
|
||||
{last_observed_block_id}"
|
||||
)
|
||||
}
|
||||
TransactionOutcome::TransportError { operation, message } => {
|
||||
panic!(
|
||||
"local sequencer transport error during {operation} for {tx_hash}: {message}"
|
||||
)
|
||||
}
|
||||
TransactionOutcome::WireMismatch { .. } => {
|
||||
panic!("local sequencer echoed different transaction bytes for {tx_hash}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_sequencer(&mut self) -> &mut LocalSequencer {
|
||||
if self.sequencer.is_none() || self.dirty {
|
||||
self.sequencer = Some(
|
||||
LocalSequencer::spawn(&self.inner)
|
||||
.unwrap_or_else(|err| panic!("failed to start local sequencer: {err}")),
|
||||
);
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
match &mut self.sequencer {
|
||||
Some(sequencer) => sequencer,
|
||||
None => unreachable!("local sequencer should be initialized"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_affected_accounts_match(&self, tx: &RpcTransaction) {
|
||||
let Some(sequencer) = &self.sequencer else {
|
||||
return;
|
||||
};
|
||||
|
||||
let account_ids = tx.affected_public_account_ids();
|
||||
let sequencer_accounts = sequencer
|
||||
.get_accounts_by_id(&account_ids)
|
||||
.unwrap_or_else(|err| panic!("local sequencer batch getAccount failed: {err}"));
|
||||
for (account_id, sequencer_account) in account_ids.into_iter().zip(sequencer_accounts) {
|
||||
let account = self.inner.get_account_by_id(account_id);
|
||||
assert_eq!(
|
||||
sequencer_account, account,
|
||||
"local sequencer account state diverged for {account_id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MirrorOutcome {
|
||||
Committed(ObservedBlock),
|
||||
NotCommitted(RejectionContext),
|
||||
}
|
||||
|
||||
struct RejectionContext {
|
||||
validation_context: Option<ObservedBlock>,
|
||||
}
|
||||
|
||||
impl RejectionContext {
|
||||
const fn precheck() -> Self {
|
||||
Self {
|
||||
validation_context: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ObservedBlock {
|
||||
block_id: u64,
|
||||
timestamp: Timestamp,
|
||||
}
|
||||
|
||||
fn ensure_risc0_dev_mode() -> DynResult<()> {
|
||||
if let Some(value) = env::var_os("RISC0_DEV_MODE") {
|
||||
let value = value.to_string_lossy();
|
||||
if matches!(value.trim(), "0" | "false") {
|
||||
return Err(io::Error::other(format!(
|
||||
"RISC0_DEV_MODE={value} disables dev mode, but the local sequencer harness requires it; unset RISC0_DEV_MODE or set it to 1"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_release_guest_builds() -> DynResult<()> {
|
||||
if let Some(value) = env::var_os(RISC0_BUILD_DEBUG_ENV) {
|
||||
let value = value.to_string_lossy();
|
||||
if value.trim() == "1" {
|
||||
return Err(io::Error::other(format!(
|
||||
"{RISC0_BUILD_DEBUG_ENV}=1 enables debug-profile guest ELFs, but local sequencer tests require release-profile guest ELFs; unset {RISC0_BUILD_DEBUG_ENV} or set it to 0"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct LocalSequencer {
|
||||
// Fields drop in declaration order; shut the node down before seed cleanup.
|
||||
_node: TestNode,
|
||||
client: TestNodeClient,
|
||||
_seed_dir: SeedDirGuard,
|
||||
}
|
||||
|
||||
impl LocalSequencer {
|
||||
fn spawn(state: &nssa::V03State) -> DynResult<Self> {
|
||||
ensure_risc0_dev_mode()?;
|
||||
ensure_release_guest_builds()?;
|
||||
let seed_dir = SeedDirGuard::from_state(state)?;
|
||||
let config = TestNodeConfig {
|
||||
state: Some(seed_dir.path().to_path_buf()),
|
||||
timeout_sec: HEALTH_TIMEOUT.as_secs(),
|
||||
..Default::default()
|
||||
};
|
||||
let node = TestNode::start(scaffold_project(), &config)
|
||||
.map_err(|err| io::Error::other(format!("scaffold test-node start failed: {err}")))?;
|
||||
Ok(Self {
|
||||
client: node.client(),
|
||||
_node: node,
|
||||
_seed_dir: seed_dir,
|
||||
})
|
||||
}
|
||||
|
||||
fn submit_and_wait(
|
||||
&self,
|
||||
tx: &RpcTransaction,
|
||||
timeout: Duration,
|
||||
) -> DynResult<TransactionOutcome> {
|
||||
let bytes = TransactionBytes::borsh(borsh::to_vec(tx)?);
|
||||
Ok(self.client.submit_and_wait(
|
||||
&bytes,
|
||||
&WaitOptions {
|
||||
timeout,
|
||||
rejection_blocks: 1,
|
||||
poll_interval: POLL_INTERVAL,
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn block_context(&self, block_id: u64) -> DynResult<ObservedBlock> {
|
||||
let block = self.client.block_info(block_id)?.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("sequencer block {block_id} was not found"),
|
||||
)
|
||||
})?;
|
||||
Ok(ObservedBlock {
|
||||
block_id: block.block_id,
|
||||
timestamp: block.timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_account_by_id(&self, account_id: AccountId) -> DynResult<Account> {
|
||||
let read = self
|
||||
.client
|
||||
.account(&account_id.to_string(), ReadAt::Latest)?;
|
||||
account_from_value(account_id, read.value)
|
||||
}
|
||||
|
||||
fn get_accounts_by_id(&self, account_ids: &[AccountId]) -> DynResult<Vec<Account>> {
|
||||
let ids = account_ids
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
let read = self.client.accounts(&ids, ReadAt::Latest)?;
|
||||
read.accounts
|
||||
.into_iter()
|
||||
.zip(account_ids)
|
||||
.map(|(entry, account_id)| account_from_value(*account_id, entry.value))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_proof_for_commitment(
|
||||
&self,
|
||||
commitment: &Commitment,
|
||||
) -> DynResult<Option<MembershipProof>> {
|
||||
let commitment_hex = hex_encode(&commitment.to_byte_array());
|
||||
let read = self.client.proof(&commitment_hex, ReadAt::Latest)?;
|
||||
read.proof.map(proof_from_value).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
struct SeedDirGuard {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl SeedDirGuard {
|
||||
fn from_state(state: &nssa::V03State) -> DynResult<Self> {
|
||||
let path = local_sequencer_seed_dir()?;
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(&path)?;
|
||||
}
|
||||
fs::create_dir_all(&path)?;
|
||||
seed_sequencer_state(&path, state)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SeedDirGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BorshSerialize)]
|
||||
struct NssaStateCellRef<'state>(&'state nssa::V03State);
|
||||
|
||||
fn seed_sequencer_state(seed_dir: &Path, state: &nssa::V03State) -> DynResult<()> {
|
||||
// Scaffold snapshot seeding cannot represent full public account data at
|
||||
// this LEZ pin, while these fixtures use force-inserted public accounts.
|
||||
let mut cf_opts = Options::default();
|
||||
cf_opts.set_max_write_buffer_number(16);
|
||||
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);
|
||||
|
||||
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,
|
||||
seed_dir.join("rocksdb"),
|
||||
vec![cfb, cfmeta, cfstate],
|
||||
)?;
|
||||
let state_column = db
|
||||
.cf_handle(CF_NSSA_STATE_NAME)
|
||||
.ok_or_else(|| io::Error::other("state column family not created"))?;
|
||||
db.put_cf(
|
||||
&state_column,
|
||||
borsh::to_vec(&DB_NSSA_STATE_KEY)?,
|
||||
borsh::to_vec(&NssaStateCellRef(state))?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scaffold_project() -> &'static Project {
|
||||
static PROJECT: OnceLock<Project> = OnceLock::new();
|
||||
|
||||
PROJECT.get_or_init(|| {
|
||||
let root = repo_root();
|
||||
let project = Project::open(&root).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to open scaffold project at {}: {err}",
|
||||
root.display()
|
||||
)
|
||||
});
|
||||
let circuits_version = env::var(TEST_NODE_CIRCUITS_VERSION_ENV)
|
||||
.unwrap_or_else(|_| DEFAULT_CIRCUITS_VERSION.to_owned());
|
||||
logos_scaffold::api::testnode::prepare_test_node(
|
||||
&project,
|
||||
&PinOverrides {
|
||||
circuits_version: Some(circuits_version),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("failed to prepare scaffold test node: {err}"));
|
||||
project
|
||||
})
|
||||
}
|
||||
|
||||
fn repo_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.expect("integration_tests crate lives under programs/integration_tests")
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn local_sequencer_seed_dir() -> DynResult<PathBuf> {
|
||||
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
||||
Ok(repo_root().join(format!(
|
||||
"target/local-sequencer/seeds/{}-{timestamp}",
|
||||
std::process::id()
|
||||
)))
|
||||
}
|
||||
|
||||
fn account_from_value(account_id: AccountId, value: AccountValue) -> DynResult<Account> {
|
||||
match value {
|
||||
AccountValue::Missing => Ok(Account::default()),
|
||||
AccountValue::Present { encoded, .. } => {
|
||||
let bytes = BASE64_STANDARD.decode(encoded)?;
|
||||
Ok(borsh::from_slice(&bytes)?)
|
||||
}
|
||||
AccountValue::DecodeError { message, .. } => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("sequencer account {account_id} could not be decoded: {message}"),
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn proof_from_value(proof: ProofValue) -> DynResult<MembershipProof> {
|
||||
let leaf_index = usize::try_from(proof.leaf_index).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("proof leaf index {} does not fit usize", proof.leaf_index),
|
||||
)
|
||||
})?;
|
||||
let path = proof
|
||||
.path
|
||||
.iter()
|
||||
.map(|node| parse_hex_32(node))
|
||||
.collect::<DynResult<Vec<_>>>()?;
|
||||
Ok((leaf_index, path))
|
||||
}
|
||||
|
||||
fn parse_hex_32(value: &str) -> DynResult<[u8; 32]> {
|
||||
let bytes = value.as_bytes();
|
||||
if bytes.len() != 64 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("expected 64 hex chars, got {}", bytes.len()),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut out = [0_u8; 32];
|
||||
for (index, chunk) in bytes.chunks_exact(2).enumerate() {
|
||||
let slot = out
|
||||
.get_mut(index)
|
||||
.ok_or_else(|| io::Error::other("hex output index out of bounds"))?;
|
||||
let pair = std::str::from_utf8(chunk)?;
|
||||
*slot = u8::from_str_radix(pair, 16)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[derive(BorshSerialize)]
|
||||
enum RpcTransaction {
|
||||
Public(Box<PublicTransaction>),
|
||||
PrivacyPreserving(Box<PrivacyPreservingTransaction>),
|
||||
ProgramDeployment(Box<ProgramDeploymentTransaction>),
|
||||
}
|
||||
|
||||
impl RpcTransaction {
|
||||
fn hash(&self) -> [u8; 32] {
|
||||
match self {
|
||||
Self::Public(tx) => tx.hash(),
|
||||
Self::PrivacyPreserving(tx) => tx.hash(),
|
||||
Self::ProgramDeployment(tx) => tx.hash(),
|
||||
}
|
||||
}
|
||||
|
||||
fn affected_public_account_ids(&self) -> Vec<AccountId> {
|
||||
match self {
|
||||
Self::Public(tx) => tx.affected_public_account_ids(),
|
||||
Self::PrivacyPreserving(tx) => tx.affected_public_account_ids(),
|
||||
Self::ProgramDeployment(tx) => tx.affected_public_account_ids(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_encode(bytes: &[u8]) -> String {
|
||||
let mut output = String::with_capacity(bytes.len().saturating_mul(2));
|
||||
for byte in bytes {
|
||||
write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
|
||||
}
|
||||
output
|
||||
}
|
||||
@ -8,10 +8,11 @@ use amm_core::{
|
||||
MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
|
||||
use integration_tests::TestState as V03State;
|
||||
use nssa::{
|
||||
error::LeeError,
|
||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction,
|
||||
};
|
||||
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ata_core::{compute_ata_seed, get_associated_token_account_id};
|
||||
use integration_tests::TestState as V03State;
|
||||
use nssa::{
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{
|
||||
@ -8,7 +9,7 @@ use nssa::{
|
||||
},
|
||||
program::Program,
|
||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey, V03State,
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use integration_tests::TestState as V03State;
|
||||
use nssa::{
|
||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction,
|
||||
};
|
||||
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
||||
use stablecoin_core::{compute_position_pda, compute_position_vault_pda, Position};
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use integration_tests::TestState as V03State;
|
||||
use nssa::{
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{Message, PrivacyPreservingTransaction, WitnessSet},
|
||||
program::Program,
|
||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey, V03State,
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
||||
|
||||
25
scaffold.toml
Normal file
25
scaffold.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[scaffold]
|
||||
version = "0.2.0"
|
||||
|
||||
[repos.lez]
|
||||
source = "https://github.com/logos-blockchain/logos-execution-zone.git"
|
||||
pin = "cf3639d8252040d13b3d4e933feb19b42c76e14a"
|
||||
|
||||
[repos.spel]
|
||||
source = "https://github.com/logos-co/spel.git"
|
||||
pin = "84f50d4aa473a70b72a16a7fb468c5618277cdd7"
|
||||
|
||||
[wallet]
|
||||
home_dir = ".scaffold/wallet"
|
||||
|
||||
[framework]
|
||||
kind = "default"
|
||||
version = "0.1.0"
|
||||
|
||||
[framework.idl]
|
||||
spec = "lssa-idl/0.1.0"
|
||||
path = "artifacts"
|
||||
|
||||
[localnet]
|
||||
port = 3040
|
||||
risc0_dev_mode = true
|
||||
Loading…
x
Reference in New Issue
Block a user