diff --git a/Cargo.lock b/Cargo.lock index 71698dfc4..7b7d629c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1985,6 +1985,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b486ab61634f05b11b591c38c71fb25139cb55e22be4fb6ecf649cc3736c074a" +dependencies = [ + "num", + "serde", + "serde_derive", +] + [[package]] name = "funty" version = "2.0.0" @@ -3609,6 +3620,7 @@ dependencies = [ "chrono", "crc32fast", "derive_more", + "fraction", "getrandom 0.2.14", "hex", "itertools 0.10.5", @@ -4020,6 +4032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" dependencies = [ "num-traits", + "serde", ] [[package]] @@ -4070,6 +4083,7 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", + "serde", ] [[package]] diff --git a/core/src/block/genesis.rs b/core/src/block/genesis.rs index 152782a3f..072e984de 100644 --- a/core/src/block/genesis.rs +++ b/core/src/block/genesis.rs @@ -145,7 +145,7 @@ fn consensus_state( let is_genesis = if negative_one { 0 } else { 1 }; let (blockchain_length, global_slot_since_genesis) = match CONSTRAINT_CONSTANTS.fork.as_ref() { None => (is_genesis, 0), - Some(fork) => (fork.previous_length + is_genesis, fork.previous_global_slot), + Some(fork) => (fork.previous_length + is_genesis, fork.genesis_slot), }; v2::ConsensusProofOfStakeDataConsensusStateValueStableV2 { diff --git a/core/src/constants.rs b/core/src/constants.rs index d05a63c34..fa9b1508e 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -29,7 +29,7 @@ pub const CONSTRAINT_CONSTANTS: ConstraintConstants = ConstraintConstants { pub struct ForkConstants { pub previous_state_hash: Fp, pub previous_length: u32, - pub previous_global_slot: u32, + pub genesis_slot: u32, } #[derive(Clone, Debug)] @@ -49,7 +49,7 @@ pub struct ConstraintConstants { pub struct ForkConstantsUnversioned { previous_state_hash: bigint::BigInt, previous_length: number::Int32, - previous_global_slot: number::Int32, + genesis_slot: number::Int32, } impl From<&ForkConstants> for ForkConstantsUnversioned { @@ -57,7 +57,7 @@ impl From<&ForkConstants> for ForkConstantsUnversioned { Self { previous_state_hash: fork_constants.previous_state_hash.into(), previous_length: fork_constants.previous_length.into(), - previous_global_slot: fork_constants.previous_global_slot.into(), + genesis_slot: fork_constants.genesis_slot.into(), } } } @@ -125,6 +125,6 @@ pub fn grace_period_end(constants: &v2::MinaBaseProtocolConstantsCheckedValueSta }; match CONSTRAINT_CONSTANTS.fork.as_ref() { None => slots, - Some(fork) => slots + fork.previous_global_slot, + Some(fork) => slots + fork.genesis_slot, } } diff --git a/core/src/snark/snark.rs b/core/src/snark/snark.rs index 41abb0707..b7830f492 100644 --- a/core/src/snark/snark.rs +++ b/core/src/snark/snark.rs @@ -1,9 +1,6 @@ use std::sync::Arc; -use mina_p2p_messages::binprot::{ - self, - macros::{BinProtRead, BinProtWrite}, -}; +use mina_p2p_messages::binprot::macros::{BinProtRead, BinProtWrite}; use mina_p2p_messages::v2::{ CurrencyFeeStableV1, MinaBaseFeeWithProverStableV1, MinaStateBlockchainStateValueStableV2LedgerProofStatement, MinaStateSnarkedLedgerStateStableV2, diff --git a/core/src/snark/snark_info.rs b/core/src/snark/snark_info.rs index bb7993e16..c9222d0ab 100644 --- a/core/src/snark/snark_info.rs +++ b/core/src/snark/snark_info.rs @@ -1,7 +1,4 @@ -use mina_p2p_messages::binprot::{ - self, - macros::{BinProtRead, BinProtWrite}, -}; +use mina_p2p_messages::binprot::macros::{BinProtRead, BinProtWrite}; use mina_p2p_messages::v2::{CurrencyFeeStableV1, NonZeroCurvePoint}; use serde::{Deserialize, Serialize}; diff --git a/core/src/snark/snark_job_commitment.rs b/core/src/snark/snark_job_commitment.rs index ab02ee115..74a65b513 100644 --- a/core/src/snark/snark_job_commitment.rs +++ b/core/src/snark/snark_job_commitment.rs @@ -1,7 +1,4 @@ -use mina_p2p_messages::binprot::{ - self, - macros::{BinProtRead, BinProtWrite}, -}; +use mina_p2p_messages::binprot::macros::{BinProtRead, BinProtWrite}; use mina_p2p_messages::v2::{CurrencyFeeStableV1, NonZeroCurvePoint}; use redux::Timestamp; use serde::{Deserialize, Serialize}; diff --git a/core/src/snark/snark_job_id.rs b/core/src/snark/snark_job_id.rs index a24ac9d4f..a8707bbfd 100644 --- a/core/src/snark/snark_job_id.rs +++ b/core/src/snark/snark_job_id.rs @@ -1,9 +1,6 @@ use std::str::FromStr; -use mina_p2p_messages::binprot::{ - self, - macros::{BinProtRead, BinProtWrite}, -}; +use mina_p2p_messages::binprot::macros::{BinProtRead, BinProtWrite}; use mina_p2p_messages::v2::{ LedgerHash, MinaStateBlockchainStateValueStableV2LedgerProofStatementSource, TransactionSnarkWorkTStableV2Proofs, diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index 132379e56..c4ea50b1a 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -70,6 +70,7 @@ crc32fast = "1" chrono = "0.4" serde_with = "3.6.1" anyhow = "1.0.75" +fraction = { version = "=0.15.1", default-features = false, features = ["with-serde-support"] } [target.'cfg(target_family = "wasm")'.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/ledger/src/account/account.rs b/ledger/src/account/account.rs index 40ee7584c..e2b02764d 100644 --- a/ledger/src/account/account.rs +++ b/ledger/src/account/account.rs @@ -2,7 +2,10 @@ use std::{fmt::Write, io::Cursor, str::FromStr}; use ark_ff::{BigInteger256, One, UniformRand, Zero}; use mina_hasher::Fp; -use mina_p2p_messages::binprot::{BinProtRead, BinProtWrite}; +use mina_p2p_messages::{ + binprot::{BinProtRead, BinProtWrite}, + v2, +}; use mina_signer::CompressedPubKey; use rand::{prelude::ThreadRng, seq::SliceRandom, Rng}; use serde::{Deserialize, Serialize}; @@ -386,7 +389,7 @@ impl ToFieldElements for ProofVerified { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct VerificationKey { pub max_proofs_verified: ProofVerified, pub actual_wrap_domain_size: ProofVerified, @@ -750,39 +753,126 @@ impl ZkAppAccount { } } +/// An `AccountId` implementing `Ord` & `PartialOrd`, reproducing OCaml ordering. +/// +/// We compare them using `BigInteger256`, not `Fp`. +/// This is a different type than `AccountId` because we want to keep the +/// `BigInteger256` values around, without re-computing them every time +/// `::partial_cmp` is called. +/// +/// So far this is used only when `AccountId` are used as keys in BTreeMap, in zkapp application. #[derive(Clone, Eq)] -pub struct AccountId { - pub public_key: CompressedPubKey, - pub token_id: TokenId, +pub struct AccountIdOrderable { + // Keep the values as `BigInteger256`. This avoid re-computing them + // every time `::partial_cmp` is called + bigint_public_key_x: BigInteger256, + bigint_public_key_is_odd: bool, + bigint_token_id: BigInteger256, + // We keep the original values, to convert back into `AccountId` without computing + public_key: CompressedPubKey, + token_id: TokenId, } -impl Ord for AccountId { +impl PartialEq for AccountIdOrderable { + fn eq(&self, other: &Self) -> bool { + let Self { + bigint_public_key_x: self_x, + bigint_public_key_is_odd: self_is_odd, + bigint_token_id: self_token_id, + public_key: _, + token_id: _, + } = self; + let Self { + bigint_public_key_x: other_x, + bigint_public_key_is_odd: other_is_odd, + bigint_token_id: other_token_id, + public_key: _, + token_id: _, + } = other; + + self_x == other_x && self_is_odd == other_is_odd && self_token_id == other_token_id + } +} +impl Ord for AccountIdOrderable { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) } } - -impl PartialOrd for AccountId { +impl PartialOrd for AccountIdOrderable { + /// Ignore `Self::public_key` and `Self::token_id` + /// We only use their bigint representations fn partial_cmp(&self, other: &Self) -> Option { - let self_pk: BigInteger256 = self.public_key.x.into(); - let other_pk: BigInteger256 = other.public_key.x.into(); - match self_pk.partial_cmp(&other_pk) { - Some(core::cmp::Ordering::Equal) | None => {} + let Self { + bigint_public_key_x: self_x, + bigint_public_key_is_odd: self_is_odd, + bigint_token_id: self_token_id, + public_key: _, + token_id: _, + } = self; + + let Self { + bigint_public_key_x: other_x, + bigint_public_key_is_odd: other_is_odd, + bigint_token_id: other_token_id, + public_key: _, + token_id: _, + } = other; + + match self_x.partial_cmp(other_x) { + None | Some(core::cmp::Ordering::Equal) => {} ord => return ord, } - - match self.public_key.is_odd.partial_cmp(&other.public_key.is_odd) { - Some(core::cmp::Ordering::Equal) | None => {} + match self_is_odd.partial_cmp(other_is_odd) { + None | Some(core::cmp::Ordering::Equal) => {} ord => return ord, } + self_token_id.partial_cmp(other_token_id) + } +} + +impl From for AccountIdOrderable { + fn from(value: AccountId) -> Self { + let AccountId { + public_key, + token_id, + } = value; + let CompressedPubKey { x, is_odd } = &public_key; + + Self { + bigint_public_key_x: (*x).into(), + bigint_public_key_is_odd: *is_odd, + bigint_token_id: token_id.0.into(), + public_key, + token_id, + } + } +} - let self_token_id: BigInteger256 = self.token_id.0.into(); - let other_token_id: BigInteger256 = other.token_id.0.into(); +impl From for AccountId { + fn from(value: AccountIdOrderable) -> Self { + let AccountIdOrderable { + bigint_public_key_x: _, + bigint_public_key_is_odd: _, + bigint_token_id: _, + public_key, + token_id, + } = value; - self_token_id.partial_cmp(&other_token_id) + Self { + public_key, + token_id, + } } } +#[derive(Clone, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] +#[serde(into = "v2::MinaBaseAccountIdStableV2")] +#[serde(from = "v2::MinaBaseAccountIdStableV2")] +pub struct AccountId { + pub public_key: CompressedPubKey, + pub token_id: TokenId, +} + impl ToInputs for AccountId { fn to_inputs(&self, inputs: &mut Inputs) { let Self { @@ -1014,7 +1104,9 @@ pub struct PermsConst { } // https://github.com/MinaProtocol/mina/blob/1765ba6bdfd7c454e5ae836c49979fa076de1bea/src/lib/mina_base/account.ml#L368 -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(into = "v2::MinaBaseAccountBinableArgStableV2")] +#[serde(from = "v2::MinaBaseAccountBinableArgStableV2")] pub struct Account { pub public_key: CompressedPubKey, // Public_key.Compressed.t pub token_id: TokenId, // Token_id.t @@ -1178,6 +1270,32 @@ impl Account { zero_min_balance.neg() } + pub fn liquid_balance_at_slot(&self, global_slot: Slot) -> Balance { + match self.timing { + Timing::Untimed => self.balance, + Timing::Timed { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + } => self + .balance + .sub_amount( + account_min_balance_at_slot( + global_slot, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + initial_minimum_balance, + ) + .to_amount(), + ) + .unwrap(), + } + } + /// https://github.com/MinaProtocol/mina/blob/2ff0292b637684ce0372e7b8e23ec85404dc5091/src/lib/mina_base/account.ml#L794 pub fn has_permission_to(&self, control: ControlTag, to: PermissionTo) -> bool { match to { diff --git a/ledger/src/account/conv.rs b/ledger/src/account/conv.rs index 818e8d472..cf9ff96da 100644 --- a/ledger/src/account/conv.rs +++ b/ledger/src/account/conv.rs @@ -131,22 +131,9 @@ impl From<&MinaBaseVerificationKeyWireStableV1> for VerificationKey { let MinaBaseVerificationKeyWireStableV1 { max_proofs_verified, actual_wrap_domain_size, - wrap_index: - MinaBaseVerificationKeyWireStableV1WrapIndex { - sigma_comm, - coefficients_comm, - generic_comm, - psm_comm, - complete_add_comm, - mul_comm, - emul_comm, - endomul_scalar_comm, - }, + wrap_index, } = vk; - let sigma = array_into(sigma_comm); - let coefficients = array_into(coefficients_comm); - VerificationKey { max_proofs_verified: match max_proofs_verified { PicklesBaseProofsVerifiedStableV1::N0 => ProofVerified::N0, @@ -158,21 +145,46 @@ impl From<&MinaBaseVerificationKeyWireStableV1> for VerificationKey { PicklesBaseProofsVerifiedStableV1::N1 => ProofVerified::N1, PicklesBaseProofsVerifiedStableV1::N2 => ProofVerified::N2, }, - wrap_index: PlonkVerificationKeyEvals { - sigma, - coefficients, - generic: generic_comm.into(), - psm: psm_comm.into(), - complete_add: complete_add_comm.into(), - mul: mul_comm.into(), - emul: emul_comm.into(), - endomul_scalar: endomul_scalar_comm.into(), - }, + wrap_index: wrap_index.into(), wrap_vk: None, } } } +impl From<&MinaBaseVerificationKeyWireStableV1WrapIndex> for PlonkVerificationKeyEvals { + fn from(value: &MinaBaseVerificationKeyWireStableV1WrapIndex) -> Self { + let MinaBaseVerificationKeyWireStableV1WrapIndex { + sigma_comm, + coefficients_comm, + generic_comm, + psm_comm, + complete_add_comm, + mul_comm, + emul_comm, + endomul_scalar_comm, + } = value; + + let sigma = array_into(sigma_comm); + let coefficients = array_into(coefficients_comm); + + PlonkVerificationKeyEvals { + sigma, + coefficients, + generic: generic_comm.into(), + psm: psm_comm.into(), + complete_add: complete_add_comm.into(), + mul: mul_comm.into(), + emul: emul_comm.into(), + endomul_scalar: endomul_scalar_comm.into(), + } + } +} +impl From for PlonkVerificationKeyEvals { + fn from(value: MinaBaseVerificationKeyWireStableV1WrapIndex) -> Self { + (&value).into() + } +} + impl From<&VerificationKey> for MinaBaseVerificationKeyWireStableV1 { fn from(vk: &VerificationKey) -> Self { let VerificationKey { @@ -296,6 +308,11 @@ impl From<&Account> for mina_p2p_messages::v2::MinaBaseAccountBinableArgStableV2 } } } +impl From for mina_p2p_messages::v2::MinaBaseAccountBinableArgStableV2 { + fn from(account: Account) -> Self { + (&account).into() + } +} impl From<&AuthRequired> for mina_p2p_messages::v2::MinaBasePermissionsAuthRequiredStableV2 { fn from(perms: &AuthRequired) -> Self { @@ -329,6 +346,13 @@ impl From<&PlonkVerificationKeyEvals> } } } +impl From> + for mina_p2p_messages::v2::MinaBaseVerificationKeyWireStableV1WrapIndex +{ + fn from(value: PlonkVerificationKeyEvals) -> Self { + (&value).into() + } +} // // Following types were written manually @@ -532,6 +556,11 @@ impl From<&MinaBaseAccountBinableArgStableV2> for Account { } } } +impl From for Account { + fn from(account: MinaBaseAccountBinableArgStableV2) -> Self { + (&account).into() + } +} impl From for MinaBaseAccountIndexStableV1 { fn from(value: AccountIndex) -> Self { diff --git a/ledger/src/database/database.rs b/ledger/src/database/database.rs index f2380b67e..e1325aa4f 100644 --- a/ledger/src/database/database.rs +++ b/ledger/src/database/database.rs @@ -300,10 +300,7 @@ mod tests { #[cfg(target_family = "wasm")] use wasm_bindgen_test::wasm_bindgen_test as test; - use crate::{ - account::Account, - tree_version::{account_empty_legacy_hash, V1, V2}, - }; + use crate::tree_version::{account_empty_legacy_hash, V1}; use super::*; diff --git a/ledger/src/lib.rs b/ledger/src/lib.rs index e38c6282c..49f94a86e 100644 --- a/ledger/src/lib.rs +++ b/ledger/src/lib.rs @@ -58,6 +58,7 @@ pub mod proofs; pub mod scan_state; pub mod sparse_ledger; pub mod staged_ledger; +pub mod transaction_pool; mod tree; mod tree_version; mod util; diff --git a/ledger/src/ondisk/database.rs b/ledger/src/ondisk/database.rs index 4dc66a063..1438362fe 100644 --- a/ledger/src/ondisk/database.rs +++ b/ledger/src/ondisk/database.rs @@ -727,10 +727,7 @@ fn exchange_file_atomically(db_path: &Path, tmp_path: &Path) -> std::io::Result< #[cfg(test)] mod tests { use rand::{Fill, Rng}; - use std::{ - path::PathBuf, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, - }; + use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; use super::*; diff --git a/ledger/src/port_ocaml/hash.rs b/ledger/src/port_ocaml/hash.rs index 21d6f151c..f39d0aca0 100644 --- a/ledger/src/port_ocaml/hash.rs +++ b/ledger/src/port_ocaml/hash.rs @@ -129,8 +129,6 @@ mod tests { use mina_signer::CompressedPubKey; - use crate::AccountId; - use super::*; #[test] diff --git a/ledger/src/proofs/block.rs b/ledger/src/proofs/block.rs index 557076c62..090aec441 100644 --- a/ledger/src/proofs/block.rs +++ b/ledger/src/proofs/block.rs @@ -549,16 +549,12 @@ mod vrf { checked_verify_merkle_path, proofs::{ field::GroupAffine, - numbers::nat::{CheckedNat, CheckedSlot}, transaction::{ decompress_var, field_to_bits, legacy_input::to_bits, scale_known, scale_non_constant, InnerCurve, }, }, - scan_state::{ - currency::{Amount, Balance}, - transaction_logic::protocol_state::EpochLedger, - }, + scan_state::currency::{Amount, Balance}, sparse_ledger::SparseLedger, AccountIndex, Address, }; @@ -760,13 +756,13 @@ pub mod consensus { proofs::{ numbers::{ currency::CheckedCurrency, - nat::{CheckedN, CheckedN32, CheckedNat, CheckedSlot, CheckedSlotSpan}, + nat::{CheckedN, CheckedN32, CheckedSlotSpan}, }, transaction::{compress_var, create_shifted_inner_curve, CompressedPubKeyVar}, }, scan_state::{ currency::{Amount, Length}, - transaction_logic::protocol_state::{EpochData, EpochLedger}, + transaction_logic::protocol_state::EpochData, }, Account, }; diff --git a/ledger/src/proofs/gates.rs b/ledger/src/proofs/gates.rs index c1bdbcff0..344c9e8f1 100644 --- a/ledger/src/proofs/gates.rs +++ b/ledger/src/proofs/gates.rs @@ -267,6 +267,7 @@ pub fn read_constraints_data( ) -> Option<(InternalVars, Vec>>)> { use mina_p2p_messages::bigint::BigInt; + #[allow(non_local_definitions)] impl From<&VRaw> for V { fn from(value: &VRaw) -> Self { match value { diff --git a/ledger/src/proofs/group_map.rs b/ledger/src/proofs/group_map.rs index b3251c1e7..919f1063d 100644 --- a/ledger/src/proofs/group_map.rs +++ b/ledger/src/proofs/group_map.rs @@ -180,10 +180,8 @@ pub fn wrap(potential_xs: (F, F, F), w: &mut Witness) -> Gro mod tock { use super::*; - use std::ops::Neg; use ark_ff::{SquareRootField, Zero}; - use mina_hasher::Fp; /// A good name from OCaml #[derive(Clone, Debug)] diff --git a/ledger/src/proofs/merge.rs b/ledger/src/proofs/merge.rs index cddc61fab..8855b23b0 100644 --- a/ledger/src/proofs/merge.rs +++ b/ledger/src/proofs/merge.rs @@ -175,11 +175,10 @@ pub(super) fn generate_merge_proof( let (s1, s2) = merge_main(&statement_with_sok, proofs, w); - let proofs: [&v2::PicklesProofProofsVerified2ReprStableV2; 2] = { + let [p1, p2]: [&v2::PicklesProofProofsVerified2ReprStableV2; 2] = { let [p1, p2] = proofs; [&p1.0.proof, &p2.0.proof] }; - let [p1, p2] = proofs; let prev_challenge_polynomial_commitments = extract_recursion_challenges(&[p1, p2]); diff --git a/ledger/src/proofs/step.rs b/ledger/src/proofs/step.rs index 30bd0c476..f1990f085 100644 --- a/ledger/src/proofs/step.rs +++ b/ledger/src/proofs/step.rs @@ -240,16 +240,14 @@ pub mod step_verifier { use super::*; use crate::proofs::{ - field::{field, CircuitVar, ToBoolean}, + field::{field, ToBoolean}, opt_sponge::OptSponge, public_input::plonk_checks::{self, ft_eval0_checked}, - transaction::{poseidon::Sponge, scalar_challenge, ReducedMessagesForNextStepProof}, + transaction::{poseidon::Sponge, scalar_challenge}, unfinalized, util::{ challenge_polynomial_checked, proof_evaluation_to_list_opt, to_absorption_sequence_opt, }, - verifier_index::wrap_domains, - witness::Witness, wrap::{ make_scalars_env_checked, one_hot_vector, ones_vector, pcs_batch::PcsBatch, @@ -2669,6 +2667,8 @@ pub fn step( ) .collect::>(); + std::mem::drop(srs); + let messages_for_next_step_proof = { let msg = MessagesForNextStepProof { app_state: Rc::clone(&app_state), diff --git a/ledger/src/proofs/transaction.rs b/ledger/src/proofs/transaction.rs index 5c0a1cada..5940d1882 100644 --- a/ledger/src/proofs/transaction.rs +++ b/ledger/src/proofs/transaction.rs @@ -497,6 +497,25 @@ pub struct PlonkVerificationKeyEvals { pub endomul_scalar: InnerCurve, } +impl<'de> serde::Deserialize<'de> for PlonkVerificationKeyEvals { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + v2::MinaBaseVerificationKeyWireStableV1WrapIndex::deserialize(deserializer).map(Self::from) + } +} + +impl serde::Serialize for PlonkVerificationKeyEvals { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let v: v2::MinaBaseVerificationKeyWireStableV1WrapIndex = self.into(); + v.serialize(serializer) + } +} + // Here cvars are not used correctly, but it's just temporary #[derive(Clone, Debug)] pub struct CircuitPlonkVerificationKeyEvals { @@ -1624,7 +1643,6 @@ pub mod legacy_input { pub mod poseidon { use std::marker::PhantomData; - use mina_poseidon::constants::PlonkSpongeConstantsKimchi; use mina_poseidon::constants::SpongeConstants; use mina_poseidon::poseidon::{ArithmeticSpongeParams, SpongeState}; @@ -2191,16 +2209,13 @@ pub mod transaction_snark { transaction::legacy_input::CheckedLegacyInput, }, scan_state::{ - currency::Sgn, fee_excess::CheckedFeeExcess, - pending_coinbase, transaction_logic::{checked_cons_signed_command_payload, Coinbase}, }, sparse_ledger::SparseLedger, AccountId, Inputs, PermissionTo, PermsConst, Timing, TimingAsRecordChecked, ToInputs, }; use ark_ff::Zero; - use mina_signer::PubKey; use crate::scan_state::{ currency, @@ -4027,10 +4042,6 @@ pub(super) fn generate_tx_proof( #[cfg(test)] mod tests_with_wasm { - use std::str::FromStr; - - use mina_hasher::Fp; - #[cfg(target_family = "wasm")] use wasm_bindgen_test::wasm_bindgen_test as test; @@ -4077,22 +4088,17 @@ mod tests_with_wasm { mod tests { use std::path::Path; - use mina_hasher::Fp; use mina_p2p_messages::binprot::{ self, macros::{BinProtRead, BinProtWrite}, }; - use crate::{ - proofs::{ - block::{generate_block_proof, BlockParams}, - constants::{StepBlockProof, StepMergeProof}, - gates::{get_provers, Provers}, - merge::{generate_merge_proof, MergeParams}, - util::sha256_sum, - zkapp::{generate_zkapp_proof, LedgerProof, ZkappParams}, - }, - scan_state::scan_state::transaction_snark::SokMessage, + use crate::proofs::{ + block::{generate_block_proof, BlockParams}, + constants::{StepBlockProof, StepMergeProof}, + gates::{get_provers, Provers}, + merge::{generate_merge_proof, MergeParams}, + zkapp::{generate_zkapp_proof, LedgerProof, ZkappParams}, }; use super::*; diff --git a/ledger/src/proofs/unfinalized.rs b/ledger/src/proofs/unfinalized.rs index 8ede9993c..46574deb4 100644 --- a/ledger/src/proofs/unfinalized.rs +++ b/ledger/src/proofs/unfinalized.rs @@ -449,9 +449,6 @@ impl Check for Unfinalized { #[cfg(test)] mod tests { - - use mina_hasher::Fp; - use super::*; #[test] diff --git a/ledger/src/proofs/wrap.rs b/ledger/src/proofs/wrap.rs index edcc8911d..fdc4782b6 100644 --- a/ledger/src/proofs/wrap.rs +++ b/ledger/src/proofs/wrap.rs @@ -1365,7 +1365,7 @@ pub mod pcs_batch { } pub mod wrap_verifier { - use std::{convert::identity, ops::Neg, sync::Arc}; + use std::{convert::identity, sync::Arc}; use itertools::Itertools; use kimchi::prover_index::ProverIndex; @@ -1374,10 +1374,7 @@ pub mod wrap_verifier { use crate::proofs::{ public_input::plonk_checks::{self, ft_eval0_checked}, step::Opt, - transaction::{ - scalar_challenge::{self, to_field_checked}, - InnerCurve, - }, + transaction::scalar_challenge::{self, to_field_checked}, unfinalized, util::{challenge_polynomial_checked, to_absorption_sequence_opt}, verifier_index::wrap_domains, @@ -1894,8 +1891,6 @@ pub mod wrap_verifier { } pub mod split_commitments { - use crate::proofs::transaction::scalar_challenge; - use super::*; #[derive(Clone, Debug)] diff --git a/ledger/src/proofs/zkapp.rs b/ledger/src/proofs/zkapp.rs index b3735f5af..4b943c5b1 100644 --- a/ledger/src/proofs/zkapp.rs +++ b/ledger/src/proofs/zkapp.rs @@ -36,7 +36,7 @@ use crate::{ local_state::{ LocalState, LocalStateEnv, LocalStateSkeleton, StackFrame, StackFrameChecked, }, - protocol_state::{protocol_state_body_view, GlobalState, GlobalStateSkeleton}, + protocol_state::{protocol_state_body_view, GlobalStateSkeleton}, zkapp_command::{ AccountUpdate, CallForest, Control, WithHash, ZkAppCommand, ACCOUNT_UPDATE_CONS_HASH_PARAM, @@ -87,9 +87,8 @@ pub struct ZkappParams<'a> { pub proved_path: Option<&'a str>, } -mod group { +pub mod group { use super::*; - use crate::scan_state::transaction_logic::zkapp_command::{AccountUpdate, Control}; #[derive(Debug)] pub enum SegmentBasic { @@ -119,26 +118,31 @@ mod group { } #[derive(Debug)] - pub struct State { - pub global: GlobalState, - pub local: LocalStateEnv, + pub struct State { + pub global: GlobalState, + pub local: LocalState, } #[derive(Debug)] - pub struct ZkappCommandIntermediateState { + pub struct ZkappCommandIntermediateState { pub kind: Kind, pub spec: SegmentBasic, - pub state_before: State, - pub state_after: State, - pub connecting_ledger: Fp, + pub state_before: State, + pub state_after: State, + pub connecting_ledger: ConnectingLedgerHash, } - fn intermediate_state( + fn intermediate_state( kind: Kind, spec: SegmentBasic, - before: &(GlobalState, LocalStateEnv, Fp), - after: &(GlobalState, LocalStateEnv, Fp), - ) -> ZkappCommandIntermediateState { + before: &(GlobalState, LocalState, ConnectingLedgerHash), + after: &(GlobalState, LocalState, ConnectingLedgerHash), + ) -> ZkappCommandIntermediateState + where + GlobalState: Clone, + LocalState: Clone, + ConnectingLedgerHash: Clone, + { let (global_before, local_before, _) = before; let (global_after, local_after, connecting_ledger) = after; ZkappCommandIntermediateState { @@ -157,17 +161,27 @@ mod group { } // Note: Unlike OCaml, the returned value (the list) is not reversed, but we keep the same method name - pub fn group_by_zkapp_command_rev( - zkapp_command: Vec<&ZkAppCommand>, - stmtss: Vec, LocalStateEnv, Fp)>>, - ) -> Vec { - let mut zkapp_account_updatess = zkapp_command - .iter() - .map(|zkapp_command| zkapp_command.all_account_updates_list()) + pub fn group_by_zkapp_command_rev<'a, I, GlobalState, LocalState, ConnectingLedgerHash>( + zkapp_command: I, + stmtss: Vec>, + ) -> Vec> + where + I: IntoIterator, + GlobalState: Clone, + LocalState: Clone, + ConnectingLedgerHash: Clone, + { + let all_account_updates_list = zkapp_command + .into_iter() + .map(|zkapp_command| zkapp_command.all_account_updates_list()); + + let zkapp_account_updatess = std::iter::once(vec![]) + .chain(all_account_updates_list) .collect::>(); - zkapp_account_updatess.insert(0, vec![]); - let mut acc = Vec::::with_capacity(32); + let mut acc = Vec::< + ZkappCommandIntermediateState, + >::with_capacity(32); // Convert to slices, to allow matching below let zkapp_account_updatess = zkapp_account_updatess @@ -177,11 +191,16 @@ mod group { let stmtss = stmtss.iter().map(|v| v.as_slice()).collect::>(); #[rustfmt::skip] - fn group_by_zkapp_command_rev_impl( + fn group_by_zkapp_command_rev_impl( zkapp_commands: &[&[AccountUpdate]], - stmtss: &[&[(GlobalState, LocalStateEnv, Fp)]], - acc: &mut Vec, - ) { + stmtss: &[&[(G, L, C)]], + acc: &mut Vec>, + ) + where + G: Clone, + L: Clone, + C: Clone, + { use Kind::{New, Same, TwoNew}; use Control::{Proof, Signature, NoneGiven}; @@ -509,10 +528,7 @@ pub fn zkapp_command_witnesses_exn( states.insert(0, vec![states[0][0].clone()]); let states = group::group_by_zkapp_command_rev( - zkapp_commands_with_context - .iter() - .map(|v| v.zkapp_command) - .collect(), + zkapp_commands_with_context.iter().map(|v| v.zkapp_command), states, ); diff --git a/ledger/src/scan_state/conv.rs b/ledger/src/scan_state/conv.rs index 194c48a72..7bd34f356 100644 --- a/ledger/src/scan_state/conv.rs +++ b/ledger/src/scan_state/conv.rs @@ -9,7 +9,8 @@ use mina_p2p_messages::{ pseq::PaddedSeq, string::CharString, v2::{ - BlockTimeTimeStableV1, ConsensusProofOfStakeDataEpochDataNextValueVersionedValueStableV1, + self, BlockTimeTimeStableV1, + ConsensusProofOfStakeDataEpochDataNextValueVersionedValueStableV1, ConsensusProofOfStakeDataEpochDataStakingValueVersionedValueStableV1, CurrencyAmountStableV1, CurrencyBalanceStableV1, CurrencyFeeStableV1, DataHashLibStateHashStableV1, EpochSeed, LedgerProofProdStableV2, @@ -131,7 +132,7 @@ use super::{ signed_command::SignedCommand, transaction_applied::{self, TransactionApplied}, zkapp_command::{ - AccountUpdate, FeePayer, FeePayerBody, SetOrKeep, WithHash, WithStackHash, + verifiable, AccountUpdate, FeePayer, FeePayerBody, SetOrKeep, WithHash, WithStackHash, }, CoinbaseFeeTransfer, FeeTransfer, Memo, SingleFeeTransfer, Transaction, TransactionFailure, TransactionStatus, UserCommand, @@ -1214,7 +1215,6 @@ impl From<&List ) } } - /// Notes: root impl From<&List> for CallForest @@ -1244,6 +1244,156 @@ impl From<&List> } } +impl From<&v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesDataA> + for WithHash +{ + fn from(value: &v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesDataA) -> Self { + let v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesDataA { data, hash } = value; + Self { + data: data.into(), + hash: hash.into(), + } + } +} + +impl From<&WithHash> + for v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesDataA +{ + fn from(value: &WithHash) -> Self { + let WithHash:: { data, hash } = value; + Self { + data: data.into(), + hash: hash.into(), + } + } +} + +/// Notes: childs for verifiable +impl From<&List> + for CallForest<(AccountUpdate, Option>)> +{ + fn from( + value: &List, + ) -> Self { + Self( + value + .iter() + .map(|update| { + let v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAACallsA { + elt, + stack_hash, + } = update; + let v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA { + account_update: (account, vk_opt), + account_update_digest, + calls, + } = &**elt; + WithStackHash { + elt: zkapp_command::Tree { + account_update: (account.into(), vk_opt.as_ref().map(Into::into)), + account_update_digest: account_update_digest.to_field(), + calls: calls.into(), + }, + stack_hash: stack_hash.to_field(), + } + }) + .collect(), + ) + } +} +/// Notes: root for verifiable +impl From<&List> + for CallForest<(AccountUpdate, Option>)> +{ + fn from(value: &List) -> Self { + let values = value + .iter() + .map(|update| { + let v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesA { elt, stack_hash } = + update; + let v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA { + account_update: (account, vk_opt), + account_update_digest, + calls, + } = elt; + WithStackHash { + elt: zkapp_command::Tree { + account_update: (account.into(), vk_opt.as_ref().map(Into::into)), + account_update_digest: account_update_digest.to_field(), + calls: calls.into(), + }, + stack_hash: stack_hash.to_field(), + } + }) + .collect(); + + let call_forest = CallForest(values); + // There is no need to call `of_wire`, because hashes are in our serialized types (verifiables types only) + // call_forest.of_wire(&[]); + + call_forest + } +} + +/// Childs for verifiable +impl From<&CallForest<(AccountUpdate, Option>)>> + for List +{ + fn from(value: &CallForest<(AccountUpdate, Option>)>) -> Self { + value + .0 + .iter() + .map(|update| { + let (acc, opt) = &update.elt.account_update; + v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAACallsA { + elt: Box::new(v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA { + account_update: (acc.into(), opt.as_ref().map(Into::into)), + account_update_digest: + v2::MinaBaseZkappCommandCallForestMakeDigestStrAccountUpdateStableV1( + update.elt.account_update_digest.into(), + ), + calls: (&update.elt.calls).into(), + }), + stack_hash: v2::MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1( + update.stack_hash.into(), + ), + } + }) + .collect() + } +} +/// Root +impl From<&CallForest<(AccountUpdate, Option>)>> + for List +{ + fn from(value: &CallForest<(AccountUpdate, Option>)>) -> Self { + value + .0 + .iter() + .map(|update| { + let (acc, opt) = &update.elt.account_update; + v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesA { + elt: v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA { + account_update: (acc.into(), opt.as_ref().map(Into::into)), + account_update_digest: + v2::MinaBaseZkappCommandCallForestMakeDigestStrAccountUpdateStableV1( + update.elt.account_update_digest.into(), + ), + calls: (&update.elt.calls).into(), + }, + stack_hash: v2::MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1( + update.stack_hash.into(), + ), + } + }) + .collect() + + // There is no need to call `to_wire`, because hashes are in our serialized types (verifiables types only) + // value.to_wire(&mut wired); + // wired.into_iter().collect() + } +} + /// We need this trait because `mina-p2p-messages` contains different types for the same data pub trait AsAccountUpdateWithHash { fn elt(&self) -> &MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesAA; @@ -1464,6 +1614,12 @@ impl From<&Memo> for MinaBaseSignedCommandMemoStableV1 { } } +impl From for SignedCommand { + fn from(value: MinaBaseSignedCommandStableV2) -> Self { + (&value).into() + } +} + impl From<&MinaBaseSignedCommandStableV2> for SignedCommand { fn from(cmd: &MinaBaseSignedCommandStableV2) -> Self { Self { @@ -1497,6 +1653,12 @@ impl From<&MinaBaseSignedCommandStableV2> for SignedCommand { } } +impl From for MinaBaseSignedCommandStableV2 { + fn from(value: SignedCommand) -> Self { + (&value).into() + } +} + impl From<&SignedCommand> for MinaBaseSignedCommandStableV2 { fn from(cmd: &SignedCommand) -> Self { Self { @@ -1544,6 +1706,38 @@ impl From<&MinaBaseZkappCommandTStableV1WireStableV1> for zkapp_command::ZkAppCo } } +impl From for verifiable::ZkAppCommand { + fn from(value: v2::MinaBaseZkappCommandVerifiableStableV1) -> Self { + let v2::MinaBaseZkappCommandVerifiableStableV1 { + fee_payer, + account_updates, + memo, + } = &value; + + verifiable::ZkAppCommand { + fee_payer: fee_payer.into(), + account_updates: account_updates.into(), + memo: memo.into(), + } + } +} + +impl From for v2::MinaBaseZkappCommandVerifiableStableV1 { + fn from(value: verifiable::ZkAppCommand) -> Self { + let verifiable::ZkAppCommand { + fee_payer, + account_updates, + memo, + } = &value; + + v2::MinaBaseZkappCommandVerifiableStableV1 { + fee_payer: fee_payer.into(), + account_updates: account_updates.into(), + memo: memo.into(), + } + } +} + impl From<&zkapp_command::ZkAppCommand> for MinaBaseZkappCommandTStableV1WireStableV1 { fn from(cmd: &zkapp_command::ZkAppCommand) -> Self { Self { @@ -1998,52 +2192,36 @@ impl binprot::BinProtWrite for LedgerProofWithSokMessage { } } +impl From for transaction_logic::valid::UserCommand { + fn from(value: MinaBaseUserCommandStableV2) -> Self { + (&value).into() + } +} + impl From<&MinaBaseUserCommandStableV2> for transaction_logic::valid::UserCommand { fn from(value: &MinaBaseUserCommandStableV2) -> Self { match value { MinaBaseUserCommandStableV2::ZkappCommand(cmd) => { Self::ZkAppCommand(Box::new(zkapp_command::valid::ZkAppCommand { - zkapp_command: zkapp_command::ZkAppCommand { - fee_payer: (&cmd.fee_payer).into(), - account_updates: (&cmd.account_updates).into(), - memo: (&cmd.memo).into(), - }, + zkapp_command: cmd.into(), })) } MinaBaseUserCommandStableV2::SignedCommand(cmd) => { - Self::SignedCommand(Box::new(SignedCommand { - payload: transaction_logic::signed_command::SignedCommandPayload { - common: transaction_logic::signed_command::Common { - fee: (&cmd.payload.common.fee).into(), - fee_payer_pk: (&cmd.payload.common.fee_payer_pk).into(), - nonce: (&cmd.payload.common.nonce).into(), - valid_until: (&cmd.payload.common.valid_until).into(), - memo: (&cmd.payload.common.memo).into(), - }, - body: match &cmd.payload.body { - MinaBaseSignedCommandPayloadBodyStableV2::Payment(payment) => { - transaction_logic::signed_command::Body::Payment(PaymentPayload { - receiver_pk: (&payment.receiver_pk).into(), - amount: payment.amount.clone().into(), - }) - } - MinaBaseSignedCommandPayloadBodyStableV2::StakeDelegation( - delegation, - ) => { - let MinaBaseStakeDelegationStableV2::SetDelegate { new_delegate } = - &delegation; - - transaction_logic::signed_command::Body::StakeDelegation( - StakeDelegationPayload::SetDelegate { - new_delegate: new_delegate.into(), - }, - ) - } - }, - }, - signer: (&cmd.signer).into(), - signature: (&*cmd.signature).into(), - })) + Self::SignedCommand(Box::new(cmd.into())) + } + } + } +} + +impl From for MinaBaseUserCommandStableV2 { + fn from(value: transaction_logic::valid::UserCommand) -> Self { + match value { + transaction_logic::valid::UserCommand::SignedCommand(cmd) => { + MinaBaseUserCommandStableV2::SignedCommand((&*cmd).into()) + } + transaction_logic::valid::UserCommand::ZkAppCommand(cmd) => { + let zkapp_command::valid::ZkAppCommand { zkapp_command } = &*cmd; + MinaBaseUserCommandStableV2::ZkappCommand(zkapp_command.into()) } } } diff --git a/ledger/src/scan_state/currency.rs b/ledger/src/scan_state/currency.rs index 069e2a773..eb67d97d8 100644 --- a/ledger/src/scan_state/currency.rs +++ b/ledger/src/scan_state/currency.rs @@ -274,6 +274,12 @@ impl Balance { } } +impl Fee { + pub const fn of_nanomina_int_exn(int: u64) -> Self { + Self::from_u64(int) + } +} + impl Index { // TODO: Not sure if OCaml wraps around here pub fn incr(&self) -> Self { @@ -298,9 +304,22 @@ impl Nonce { self.sub_flagged(&rhs.magnitude) } } + + /// low <= self <= high + pub fn between(&self, low: &Self, high: &Self) -> bool { + low <= self && self <= high + } } impl BlockTime { + pub fn now() -> Self { + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + let elapsed: Duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let elapsed: u64 = elapsed.as_millis().try_into().unwrap(); + Self(elapsed) + } + pub fn add(&self, span: BlockTimeSpan) -> Self { Self(self.0.checked_add(span.0).unwrap()) } @@ -308,12 +327,30 @@ impl BlockTime { pub fn sub(&self, span: BlockTimeSpan) -> Self { Self(self.0.checked_sub(span.0).unwrap()) } + + pub fn diff(&self, other: Self) -> BlockTimeSpan { + BlockTimeSpan(self.0 - other.0) + } + + pub fn to_span_since_epoch(&self) -> BlockTimeSpan { + let Self(ms) = self; + BlockTimeSpan(*ms) + } + + pub fn of_span_since_epoch(span: BlockTimeSpan) -> Self { + let BlockTimeSpan(ms) = span; + Self(ms) + } } impl BlockTimeSpan { pub fn of_ms(ms: u64) -> Self { Self(ms) } + pub fn to_ms(&self) -> u64 { + let Self(ms) = self; + *ms + } } impl Slot { @@ -322,6 +359,11 @@ impl Slot { Self(self.0.wrapping_add(1)) } + pub fn add(&self, other: SlotSpan) -> Self { + let SlotSpan(other) = other; + Self(self.0.checked_add(other).unwrap()) + } + pub fn succ(&self) -> Self { self.incr() } @@ -338,7 +380,7 @@ macro_rules! impl_number { $(impl_number!({$name64, u64, as_u64, from_u64, next_u64, append_u64},);)+ }; ($({ $name:ident, $inner:ty, $as_name:ident, $from_name:ident, $next_name:ident, $append_name:ident },)*) => ($( - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] pub struct $name(pub(super) $inner); impl std::fmt::Debug for $name { @@ -531,6 +573,6 @@ macro_rules! impl_number { } impl_number!( - 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, }, + 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, Epoch, }, 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, N, }, ); diff --git a/ledger/src/scan_state/fee_rate.rs b/ledger/src/scan_state/fee_rate.rs new file mode 100644 index 000000000..8b7949e7c --- /dev/null +++ b/ledger/src/scan_state/fee_rate.rs @@ -0,0 +1,21 @@ +use crate::scan_state::currency::Magnitude; + +use super::currency::Fee; +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash, Serialize, Deserialize)] +pub struct FeeRate { + q: fraction::Fraction, +} + +impl FeeRate { + pub fn make_exn(fee: Fee, weight: u64) -> Self { + if weight == 0 { + assert!(fee.is_zero()); + } + + Self { + q: fraction::Fraction::new(fee.as_u64(), weight), + } + } +} diff --git a/ledger/src/scan_state/mod.rs b/ledger/src/scan_state/mod.rs index 4a28eb564..ad2e968f3 100644 --- a/ledger/src/scan_state/mod.rs +++ b/ledger/src/scan_state/mod.rs @@ -1,6 +1,7 @@ pub mod conv; pub mod currency; pub mod fee_excess; +pub mod fee_rate; mod parallel_scan; pub mod pending_coinbase; pub mod protocol_state; @@ -10,3 +11,30 @@ pub mod snark_work; pub mod transaction_logic; pub mod zkapp_logic; pub use parallel_scan::SpacePartition; + +pub struct GenesisConstant { + pub protocol: (), + pub txpool_max_size: usize, + pub num_accounts: Option, + pub zkapp_proof_update_cost: f64, + pub zkapp_signed_single_update_cost: f64, + pub zkapp_signed_pair_update_cost: f64, + pub zkapp_transaction_cost_limit: f64, + pub max_event_elements: usize, + pub max_action_elements: usize, + pub zkapp_cmd_limit_hardcap: usize, +} + +// TODO: Not sure if any of those values are correct +pub const GENESIS_CONSTANT: GenesisConstant = GenesisConstant { + protocol: (), + txpool_max_size: 3000, + num_accounts: None, + zkapp_proof_update_cost: 10.26, + zkapp_signed_single_update_cost: 9.14, + zkapp_signed_pair_update_cost: 10.08, + zkapp_transaction_cost_limit: 69.45, + max_event_elements: 100, + max_action_elements: 100, + zkapp_cmd_limit_hardcap: 128, +}; diff --git a/ledger/src/scan_state/parallel_scan.rs b/ledger/src/scan_state/parallel_scan.rs index 9a4c18685..65fc32d82 100644 --- a/ledger/src/scan_state/parallel_scan.rs +++ b/ledger/src/scan_state/parallel_scan.rs @@ -2595,7 +2595,6 @@ mod tests { sync::{ atomic::{AtomicBool, Ordering::Relaxed}, mpsc::{sync_channel, Receiver, SyncSender}, - Arc, }, }; diff --git a/ledger/src/scan_state/pending_coinbase.rs b/ledger/src/scan_state/pending_coinbase.rs index 59f125166..57e0bf77f 100644 --- a/ledger/src/scan_state/pending_coinbase.rs +++ b/ledger/src/scan_state/pending_coinbase.rs @@ -966,7 +966,7 @@ impl PendingCoinbaseWitness { /// Keep it a bit generic, in case we need a merkle tree somewhere else pub mod merkle_tree { - use crate::{AccountIndex, Address, AddressIterator, Direction, HashesMatrix, MerklePath}; + use crate::{AccountIndex, AddressIterator, Direction, HashesMatrix}; use super::*; @@ -1251,7 +1251,7 @@ pub mod merkle_tree { mod tests { use crate::FpExt; - use super::{merkle_tree::MiniMerkleTree, *}; + use super::*; #[cfg(target_family = "wasm")] use wasm_bindgen_test::wasm_bindgen_test as test; diff --git a/ledger/src/scan_state/transaction_logic.rs b/ledger/src/scan_state/transaction_logic.rs index 41d76b9b2..5640f9761 100644 --- a/ledger/src/scan_state/transaction_logic.rs +++ b/ledger/src/scan_state/transaction_logic.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap, HashSet}; use ark_ff::Zero; use itertools::{FoldWhile, Itertools}; @@ -10,8 +10,10 @@ use openmina_core::constants::ConstraintConstants; use crate::proofs::witness::Witness; use crate::scan_state::transaction_logic::transaction_partially_applied::FullyApplied; +use crate::scan_state::transaction_logic::zkapp_command::MaybeWithStatus; use crate::scan_state::zkapp_logic; -use crate::{hash_with_kimchi, ControlTag, Inputs}; +use crate::transaction_pool::VerificationKeyWire; +use crate::{hash_with_kimchi, AccountIdOrderable, BaseLedger, ControlTag, Inputs}; use crate::{ scan_state::transaction_logic::transaction_applied::{CommandApplied, Varying}, sparse_ledger::{LedgerIntf, SparseLedger}, @@ -32,6 +34,7 @@ use self::{ }; use super::currency::SlotSpan; +use super::fee_rate::FeeRate; use super::zkapp_logic::ZkAppCommandElt; use super::{ currency::{Amount, Balance, Fee, Index, Length, Magnitude, Nonce, Signed, Slot}, @@ -41,7 +44,7 @@ use super::{ }; /// https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/transaction_status.ml#L9 -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub enum TransactionFailure { Predicate, SourceNotPresent, @@ -167,7 +170,7 @@ impl ToString for TransactionFailure { } /// https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/transaction_status.ml#L452 -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub enum TransactionStatus { Applied, Failed(Vec>), @@ -177,10 +180,13 @@ impl TransactionStatus { pub fn is_applied(&self) -> bool { matches!(self, Self::Applied) } + pub fn is_failed(&self) -> bool { + matches!(self, Self::Failed(_)) + } } /// https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/with_status.ml#L6 -#[derive(Debug, Clone, PartialEq)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] pub struct WithStatus { pub data: T, pub status: TransactionStatus, @@ -254,7 +260,11 @@ pub mod valid { pub type SignedCommand = super::signed_command::SignedCommand; - #[derive(Clone, Debug, PartialEq)] + use serde::{Deserialize, Serialize}; + + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + #[serde(into = "MinaBaseUserCommandStableV2")] + #[serde(from = "MinaBaseUserCommandStableV2")] pub enum UserCommand { SignedCommand(Box), ZkAppCommand(Box), @@ -270,6 +280,13 @@ pub mod valid { } } } + + pub fn fee_payer(&self) -> AccountId { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee_payer(), + UserCommand::ZkAppCommand(cmd) => cmd.zkapp_command.fee_payer(), + } + } } impl GenericCommand for UserCommand { @@ -660,12 +677,12 @@ impl Memo { } pub mod signed_command { - + use mina_p2p_messages::v2::MinaBaseSignedCommandStableV2; use mina_signer::Signature; - use crate::{decompress_pk, scan_state::currency::Slot, AccountId}; + use crate::decompress_pk; - use super::{zkapp_command::AccessedOrNot, *}; + use super::*; /// https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/signed_command_payload.ml#L75 #[derive(Debug, Clone, PartialEq)] @@ -739,7 +756,27 @@ pub mod signed_command { } } - #[derive(Debug, Clone, PartialEq)] + /// https://github.com/MinaProtocol/mina/blob/1551e2faaa246c01636908aabe5f7981715a10f4/src/lib/mina_base/signed_command_payload.ml#L362 + mod weight { + use super::*; + + fn payment(_: &PaymentPayload) -> u64 { + 1 + } + fn stake_delegation(_: &StakeDelegationPayload) -> u64 { + 1 + } + pub fn of_body(body: &Body) -> u64 { + match body { + Body::Payment(p) => payment(p), + Body::StakeDelegation(s) => stake_delegation(s), + } + } + } + + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] + #[serde(into = "MinaBaseSignedCommandStableV2")] + #[serde(from = "MinaBaseSignedCommandStableV2")] pub struct SignedCommand { pub payload: SignedCommandPayload, pub signer: CompressedPubKey, // TODO: This should be a `mina_signer::PubKey` @@ -762,6 +799,15 @@ pub mod signed_command { &self.payload.common.fee_payer_pk } + pub fn weight(&self) -> u64 { + let Self { + payload: SignedCommandPayload { common: _, body }, + signer: _, + signature: _, + } = self; + weight::of_body(body) + } + /// https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/signed_command_payload.ml#L318 pub fn fee_token(&self) -> TokenId { TokenId::default() @@ -835,21 +881,25 @@ pub mod signed_command { pub mod zkapp_command { use std::sync::Arc; - use ark_ff::{UniformRand, Zero}; + use ark_ff::UniformRand; use mina_p2p_messages::v2::MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA; use mina_signer::Signature; use rand::{seq::SliceRandom, Rng}; use crate::{ - account, dummy, gen_compressed, gen_keypair, hash_noinputs, hash_with_kimchi, + account, dummy, gen_compressed, gen_keypair, hash_noinputs, proofs::{ field::{Boolean, ToBoolean}, to_field_elements::ToFieldElements, transaction::Check, }, - scan_state::currency::{Balance, Length, MinMax, Sgn, Signed, Slot, SlotSpan}, + scan_state::{ + currency::{MinMax, Sgn}, + GenesisConstant, GENESIS_CONSTANT, + }, + transaction_pool::VerificationKeyWire, zkapps::snark::zkapp_check::InSnarkCheck, - AuthRequired, ControlTag, Inputs, MyCow, Permissions, ToInputs, TokenSymbol, + AuthRequired, MyCow, Permissions, SetVerificationKey, ToInputs, TokenSymbol, VerificationKey, VotingFor, ZkAppAccount, ZkAppUri, }; @@ -862,6 +912,10 @@ pub mod zkapp_command { pub fn hash(&self) -> Fp { hash_with_kimchi("MinaZkappEvent", &self.0[..]) } + pub fn len(&self) -> usize { + let Self(list) = self; + list.len() + } } /// https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/account_update.ml#L834 @@ -1251,10 +1305,37 @@ pub mod zkapp_command { } } - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct WithHash { + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] + pub struct WithHash { pub data: T, - pub hash: Fp, + pub hash: H, + } + + impl Ord for WithHash { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.hash.cmp(&other.hash) + } + } + + impl PartialOrd for WithHash { + fn partial_cmp(&self, other: &Self) -> Option { + self.hash.partial_cmp(&other.hash) + } + } + + impl Eq for WithHash {} + + impl PartialEq for WithHash { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } + } + + impl std::hash::Hash for WithHash { + fn hash(&self, state: &mut H) { + let Self { data: _, hash } = self; + hash.hash(state); + } } impl ToFieldElements for WithHash { @@ -2684,6 +2765,10 @@ pub mod zkapp_command { ControlTag::NoneGiven => Self::NoneGiven, } } + + pub fn dummy(&self) -> Self { + Self::dummy_of_tag(self.tag()) + } } #[derive(Clone, Debug, PartialEq)] @@ -3273,9 +3358,9 @@ pub mod zkapp_command { } /// https://github.com/MinaProtocol/mina/blob/05c2f73d0f6e4f1341286843814ce02dcb3919e0/src/lib/mina_base/zkapp_command.ml#L68 - fn fold_impl(&self, init: A, fun: &mut F) -> A + fn fold_impl<'a, A, F>(&'a self, init: A, fun: &mut F) -> A where - F: FnMut(A, &AccUpdate) -> A, + F: FnMut(A, &'a AccUpdate) -> A, { let mut accum = init; for elem in self.iter() { @@ -3285,9 +3370,9 @@ pub mod zkapp_command { accum } - pub fn fold(&self, init: A, mut fun: F) -> A + pub fn fold<'a, A, F>(&'a self, init: A, mut fun: F) -> A where - F: FnMut(A, &AccUpdate) -> A, + F: FnMut(A, &'a AccUpdate) -> A, { self.fold_impl(init, &mut fun) } @@ -3422,26 +3507,11 @@ pub mod zkapp_command { } } - impl CallForest { - pub fn cons( - &self, - calls: Option>, - account_update: AccountUpdate, - ) -> Self { - let account_update_digest = account_update.digest(); - - let tree = Tree:: { - account_update, - account_update_digest, - calls: calls.unwrap_or_else(|| CallForest(Vec::new())), - }; - self.cons_tree(tree) - } - + impl CallForest { /// https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/zkapp_command.ml#L583 pub fn accumulate_hashes(&mut self, hash_account_update: &F) where - F: Fn(&AccountUpdate) -> Fp, + F: Fn(&AccUpdate) -> Fp, { /// https://github.com/MinaProtocol/mina/blob/3fe924c80a4d01f418b69f27398f5f93eb652514/src/lib/mina_base/zkapp_command.ml#L293 fn cons(hash: Fp, h_tl: Fp) -> Fp { @@ -3465,7 +3535,7 @@ pub mod zkapp_command { let elem = &mut self.0[index]; let WithStackHash { elt: - Tree:: { + Tree:: { account_update, account_update_digest, calls, @@ -3483,6 +3553,23 @@ pub mod zkapp_command { self.0[index].stack_hash = cons(node_hash, hash); } } + } + + impl CallForest { + pub fn cons( + &self, + calls: Option>, + account_update: AccountUpdate, + ) -> Self { + let account_update_digest = account_update.digest(); + + let tree = Tree:: { + account_update, + account_update_digest, + calls: calls.unwrap_or_else(|| CallForest(Vec::new())), + }; + self.cons_tree(tree) + } pub fn accumulate_hashes_predicated(&mut self) { // Note: There seems to be no difference with `accumulate_hashes` @@ -3506,6 +3593,26 @@ pub mod zkapp_command { } } + impl CallForest<(AccountUpdate, Option>)> { + // Don't implement `{from,to}_wire` because the binprot types contain the hashes + + // /// https://github.com/MinaProtocol/mina/blob/2ff0292b637684ce0372e7b8e23ec85404dc5091/src/lib/mina_base/zkapp_command.ml#L830 + // pub fn of_wire( + // &mut self, + // _wired: &[v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesA], + // ) { + // self.accumulate_hashes(&|(account_update, _vk_opt)| account_update.digest()); + // } + + // /// https://github.com/MinaProtocol/mina/blob/2ff0292b637684ce0372e7b8e23ec85404dc5091/src/lib/mina_base/zkapp_command.ml#L840 + // pub fn to_wire( + // &self, + // _wired: &mut [MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA], + // ) { + // // self.remove_callers(wired); + // } + } + /// https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/account_update.ml#L1081 #[derive(Debug, Clone, PartialEq, Eq)] pub struct FeePayerBody { @@ -3554,6 +3661,149 @@ pub mod zkapp_command { FeeExcess::of_single((self.fee_token(), Signed::::of_unsigned(self.fee()))) } + fn fee_payer_account_update(&self) -> &FeePayer { + let Self { fee_payer, .. } = self; + fee_payer + } + + pub fn applicable_at_nonce(&self) -> Nonce { + self.fee_payer_account_update().body.nonce + } + + pub fn weight(&self) -> u64 { + let Self { + fee_payer, + account_updates, + memo, + } = self; + [ + zkapp_weight::fee_payer(fee_payer), + zkapp_weight::account_updates(account_updates), + zkapp_weight::memo(memo), + ] + .iter() + .sum() + } + + pub fn has_zero_vesting_period(&self) -> bool { + self.account_updates + .iter() + .any(|p| match &p.elt.account_update.body.update.timing { + SetOrKeep::Keep => false, + SetOrKeep::Set(Timing { vesting_period, .. }) => vesting_period.is_zero(), + }) + } + + pub fn is_incompatible_version(&self) -> bool { + self.account_updates.iter().any(|p| { + match &p.elt.account_update.body.update.permissions { + SetOrKeep::Keep => false, + SetOrKeep::Set(Permissions { + set_verification_key, + .. + }) => { + let SetVerificationKey { + auth: _, + txn_version, + } = set_verification_key; + *txn_version != crate::TXN_VERSION_CURRENT + } + } + }) + } + + fn zkapp_cost( + proof_segments: usize, + signed_single_segments: usize, + signed_pair_segments: usize, + ) -> f64 { + // (*10.26*np + 10.08*n2 + 9.14*n1 < 69.45*) + let GenesisConstant { + zkapp_proof_update_cost: proof_cost, + zkapp_signed_pair_update_cost: signed_pair_cost, + zkapp_signed_single_update_cost: signed_single_cost, + .. + } = GENESIS_CONSTANT; + + (proof_cost * (proof_segments as f64)) + + (signed_pair_cost * (signed_pair_segments as f64)) + + (signed_single_cost * (signed_single_segments as f64)) + } + + /// Zkapp_command transactions are filtered using this predicate + /// - when adding to the transaction pool + /// - in incoming blocks + pub fn valid_size(&self) -> Result<(), String> { + use crate::proofs::zkapp::group::{SegmentBasic, ZkappCommandIntermediateState}; + + let Self { + account_updates, + fee_payer: _, + memo: _, + } = self; + + let events_elements = + |events: &[Event]| -> usize { events.iter().map(Event::len).sum() }; + + let mut n_account_updates = 0; + let (mut num_event_elements, mut num_action_elements) = (0, 0); + + account_updates.fold((), |_, account_update| { + num_event_elements += events_elements(account_update.body.events.events()); + num_action_elements += events_elements(account_update.body.actions.events()); + n_account_updates += 1; + }); + + let group = std::iter::repeat(((), (), ())) + .take(n_account_updates + 1) // + 1 to prepend one. See OCaml + .collect::>(); + + let groups = crate::proofs::zkapp::group::group_by_zkapp_command_rev::<_, (), (), ()>( + [self], + vec![vec![((), (), ())], group], + ); + + let (mut proof_segments, mut signed_single_segments, mut signed_pair_segments) = + (0, 0, 0); + + for ZkappCommandIntermediateState { spec, .. } in &groups { + match spec { + SegmentBasic::Proved => proof_segments += 1, + SegmentBasic::OptSigned => signed_single_segments += 1, + SegmentBasic::OptSignedOptSigned => signed_pair_segments += 1, + } + } + + let GenesisConstant { + zkapp_transaction_cost_limit: cost_limit, + max_event_elements, + max_action_elements, + .. + } = GENESIS_CONSTANT; + + let zkapp_cost_within_limit = + Self::zkapp_cost(proof_segments, signed_single_segments, signed_pair_segments) + < cost_limit; + let valid_event_elements = num_event_elements <= max_event_elements; + let valid_action_elements = num_action_elements <= max_action_elements; + + if zkapp_cost_within_limit && valid_event_elements && valid_action_elements { + return Ok(()); + } + + let err = [ + (zkapp_cost_within_limit, "zkapp transaction too expensive"), + (valid_event_elements, "too many event elements"), + (valid_action_elements, "too many action elements"), + ] + .iter() + .filter(|(b, _s)| !b) + .map(|(_b, s)| s) + .join(";"); + + Err(err) + } + /// https://github.com/MinaProtocol/mina/blob/2ff0292b637684ce0372e7b8e23ec85404dc5091/src/lib/mina_base/zkapp_command.ml#L997 pub fn account_access_statuses( &self, @@ -3606,11 +3856,11 @@ pub mod zkapp_command { } /// https://github.com/MinaProtocol/mina/blob/02c9d453576fa47f78b2c388fb2e0025c47d991c/src/lib/mina_base/zkapp_command.ml#L989 - pub fn extract_vks(&self) -> Vec> { + pub fn extract_vks(&self) -> Vec<(AccountId, WithHash)> { self.account_updates .fold(Vec::with_capacity(256), |mut acc, p| { if let SetOrKeep::Set(vk) = &p.body.update.verification_key { - acc.push(vk.clone()); + acc.push((p.account_id(), vk.clone())); }; acc }) @@ -3642,12 +3892,13 @@ pub mod zkapp_command { } pub mod verifiable { - use std::collections::HashMap; + use mina_p2p_messages::v2::MinaBaseZkappCommandVerifiableStableV1; use super::*; - use crate::VerificationKey; - #[derive(Debug, Clone)] + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] + #[serde(from = "MinaBaseZkappCommandVerifiableStableV1")] + #[serde(into = "MinaBaseZkappCommandVerifiableStableV1")] pub struct ZkAppCommand { pub fee_payer: FeePayer, pub account_updates: CallForest<(AccountUpdate, Option>)>, @@ -3729,7 +3980,7 @@ pub mod zkapp_command { /// ledger for the key (ie set by a previous transaction). pub fn create( zkapp: &super::ZkAppCommand, - status: &TransactionStatus, + is_failed: bool, find_vk: impl Fn(Fp, &AccountId) -> Result, String>, ) -> Result { let super::ZkAppCommand { @@ -3752,8 +4003,8 @@ pub mod zkapp_command { check_authorization(p)?; - match (&p.body.authorization_kind, status.is_applied()) { - (AuthorizationKind::Proof(vk_hash), true) => { + match (&p.body.authorization_kind, is_failed) { + (AuthorizationKind::Proof(vk_hash), false) => { let prioritized_vk = { // only lookup _past_ vk setting, ie exclude the new one we // potentially set in this account_update (use the non-' @@ -3810,6 +4061,9 @@ pub mod zkapp_command { pub fn forget(self) -> super::ZkAppCommand { self.zkapp_command } + pub fn forget_ref(&self) -> &super::ZkAppCommand { + &self.zkapp_command + } } /// https://github.com/MinaProtocol/mina/blob/2ff0292b637684ce0372e7b8e23ec85404dc5091/src/lib/mina_base/zkapp_command.ml#L1499 @@ -3825,119 +4079,162 @@ pub mod zkapp_command { status: &TransactionStatus, find_vk: impl Fn(Fp, &AccountId) -> Result, String>, ) -> Result { - create(&zkapp_command, status, find_vk).map(of_verifiable) + create(&zkapp_command, status.is_failed(), find_vk).map(of_verifiable) } } - pub trait Strategy { - fn create_all( - cmds: Vec>>, - find_vk: impl Fn(Fp, &AccountId) -> Result, String>, - ) -> Result>, String>; + pub struct MaybeWithStatus { + pub cmd: T, + pub status: Option, } - trait CreateAll { - type Value; + impl From> for MaybeWithStatus { + fn from(value: WithStatus) -> Self { + let WithStatus { data, status } = value; + Self { + cmd: data, + status: Some(status), + } + } + } - fn empty() -> Self; - fn find(&self, key: Fp) -> Option<&Self::Value>; - fn set(&mut self, key: Fp, value: Self::Value); + impl From> for WithStatus { + fn from(value: MaybeWithStatus) -> Self { + let MaybeWithStatus { cmd, status } = value; + Self { + data: cmd, + status: status.unwrap(), + } + } } - impl Strategy for T - where - T: CreateAll>, - { - /// https://github.com/MinaProtocol/mina/blob/02c9d453576fa47f78b2c388fb2e0025c47d991c/src/lib/mina_base/zkapp_command.ml#L1346 - fn create_all( - cmds: Vec>>, - find_vk: impl Fn(Fp, &AccountId) -> Result, String>, - ) -> Result>, String> { - let mut running_cache = Self::empty(); + impl MaybeWithStatus { + pub fn cmd(&self) -> &T { + &self.cmd + } + pub fn is_failed(&self) -> bool { + self.status + .as_ref() + .map(TransactionStatus::is_failed) + .unwrap_or(false) + } + pub fn map(self, fun: F) -> MaybeWithStatus + where + F: FnOnce(T) -> V, + { + MaybeWithStatus { + cmd: fun(self.cmd), + status: self.status, + } + } + } - Ok(cmds - .into_iter() - .map(|WithStatus { data: cmd, status }| { - let verified_cmd = verifiable::create(&cmd, &status, |vk_hash, account_id| { - // first we check if there's anything in the running - // cache within this chunk so far - - match running_cache.find(vk_hash) { - None => { - // before falling back to the find_vk - find_vk(vk_hash, account_id) - } - Some(vk) => Ok(vk.clone()), - } - }) - .unwrap(); + pub trait ToVerifiableCache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire>; + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire); + } - for vk in cmd.extract_vks() { - running_cache.set(vk.hash, vk); - } + pub trait ToVerifiableStrategy { + type Cache: ToVerifiableCache; - WithStatus { - data: verified_cmd, - status, - } - }) - .collect()) + fn create_all( + cmd: &ZkAppCommand, + is_failed: bool, + cache: &mut Self::Cache, + ) -> Result { + let verified_cmd = verifiable::create(cmd, is_failed, |vk_hash, account_id| { + cache + .find(account_id, &vk_hash) + .cloned() + .ok_or_else(|| "verification key not found in cache".to_string()) + })?; + if !is_failed { + for (account_id, vk) in cmd.extract_vks() { + cache.add(account_id, vk); + } + } + Ok(verified_cmd) } } - mod any { - use std::collections::HashMap; - + pub mod from_unapplied_sequence { use super::*; - struct Any { - inner: std::collections::HashMap, + pub struct Cache { + cache: HashMap>, } - impl super::CreateAll for Any { - type Value = T; - - fn empty() -> Self { - Self { - inner: HashMap::with_capacity(128), - } + impl Cache { + pub fn new(cache: HashMap>) -> Self { + Self { cache } } + } - fn find(&self, key: Fp) -> Option<&Self::Value> { - self.inner.get(&key) + impl ToVerifiableCache for Cache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { + let vks = self.cache.get(account_id)?; + vks.get(vk_hash) } - - fn set(&mut self, key: Fp, value: Self::Value) { - self.inner.insert(key, value); + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { + let vks = self.cache.entry(account_id).or_default(); + vks.insert(vk.hash, vk); } } + + pub struct FromUnappliedSequence; + + impl ToVerifiableStrategy for FromUnappliedSequence { + type Cache = Cache; + } } - pub mod last { + pub mod from_applied_sequence { use super::*; - pub struct Last { - inner: Option<(Fp, T)>, + pub struct Cache { + cache: HashMap, } - impl CreateAll for Last { - type Value = T; - - fn empty() -> Self { - Self { inner: None } + impl Cache { + pub fn new(cache: HashMap) -> Self { + Self { cache } } + } - fn find(&self, key: Fp) -> Option<&Self::Value> { - match self.inner.as_ref() { - Some((k, value)) if k == &key => Some(value), - _ => None, - } + impl ToVerifiableCache for Cache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { + self.cache.get(account_id).filter(|vk| &vk.hash == vk_hash) } - - fn set(&mut self, key: Fp, value: Self::Value) { - self.inner = Some((key, value)); + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { + self.cache.insert(account_id, vk); } } + + pub struct FromAppliedSequence; + + impl ToVerifiableStrategy for FromAppliedSequence { + type Cache = Cache; + } + } + + /// https://github.com/MinaProtocol/mina/blob/1551e2faaa246c01636908aabe5f7981715a10f4/src/lib/mina_base/zkapp_command.ml#L1421 + pub mod zkapp_weight { + use crate::scan_state::transaction_logic::zkapp_command::{ + AccountUpdate, CallForest, FeePayer, + }; + + pub fn account_update(_: &AccountUpdate) -> u64 { + 1 + } + pub fn fee_payer(_: &FeePayer) -> u64 { + 1 + } + pub fn account_updates(list: &CallForest) -> u64 { + list.fold(0, |acc, p| acc + account_update(p)) + } + pub fn memo(_: &super::Memo) -> u64 { + 0 + } } } @@ -4042,7 +4339,7 @@ pub mod verifiable { use super::*; - #[derive(Debug)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum UserCommand { SignedCommand(Box), ZkAppCommand(Box), @@ -4153,6 +4450,34 @@ impl UserCommand { .collect() } + pub fn fee_payer(&self) -> AccountId { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee_payer(), + UserCommand::ZkAppCommand(cmd) => cmd.fee_payer(), + } + } + + pub fn valid_until(&self) -> Slot { + match self { + UserCommand::SignedCommand(cmd) => cmd.valid_until(), + UserCommand::ZkAppCommand(cmd) => { + let ZkAppCommand { fee_payer, .. } = &**cmd; + fee_payer.body.valid_until.unwrap_or_else(Slot::max) + } + } + } + + pub fn applicable_at_nonce(&self) -> Nonce { + match self { + UserCommand::SignedCommand(cmd) => cmd.nonce(), + UserCommand::ZkAppCommand(cmd) => cmd.applicable_at_nonce(), + } + } + + pub fn expected_target_nonce(&self) -> Nonce { + self.applicable_at_nonce().succ() + } + /// https://github.com/MinaProtocol/mina/blob/05c2f73d0f6e4f1341286843814ce02dcb3919e0/src/lib/mina_base/user_command.ml#L192 pub fn fee(&self) -> Fee { match self { @@ -4161,7 +4486,26 @@ impl UserCommand { } } - pub fn extract_vks(&self) -> Vec> { + pub fn weight(&self) -> u64 { + match self { + UserCommand::SignedCommand(cmd) => cmd.weight(), + UserCommand::ZkAppCommand(cmd) => cmd.weight(), + } + } + + /// Fee per weight unit + pub fn fee_per_wu(&self) -> FeeRate { + FeeRate::make_exn(self.fee(), self.weight()) + } + + pub fn fee_token(&self) -> TokenId { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee_token(), + UserCommand::ZkAppCommand(cmd) => cmd.fee_token(), + } + } + + pub fn extract_vks(&self) -> Vec<(AccountId, WithHash)> { match self { UserCommand::SignedCommand(_) => vec![], UserCommand::ZkAppCommand(zkapp) => zkapp.extract_vks(), @@ -4193,71 +4537,172 @@ impl UserCommand { match self { UserCommand::SignedCommand(cmd) => Ok(SignedCommand(cmd.clone())), UserCommand::ZkAppCommand(zkapp) => Ok(ZkAppCommand(Box::new( - zkapp_command::verifiable::create(zkapp, status, find_vk)?, + zkapp_command::verifiable::create(zkapp, status.is_failed(), find_vk)?, ))), } } + pub fn load_vks_from_ledger( + account_ids: HashSet, + ledger: &crate::Mask, + ) -> HashMap { + let ids: Vec<_> = account_ids.iter().cloned().collect(); + let locations: Vec<_> = ledger + .location_of_account_batch(&ids) + .into_iter() + .filter_map(|(_, addr)| addr) + .collect(); + ledger + .get_batch(&locations) + .into_iter() + .filter_map(|(_, account)| { + let account = account.unwrap(); + let zkapp = account.zkapp.as_ref()?; + let vk = zkapp.verification_key.clone()?; + + // TODO: The account should contains the `WithHash` + let hash = vk.hash(); + let vk = WithHash { data: vk, hash }; + + Some((account.id(), vk)) + }) + .collect() + } + + pub fn load_vks_from_ledger_accounts( + accounts: &BTreeMap, + ) -> HashMap { + accounts + .iter() + .filter_map(|(_, account)| { + let zkapp = account.zkapp.as_ref()?; + let vk = zkapp.verification_key.clone()?; + + // TODO: The account should contains the `WithHash` + let hash = vk.hash(); + let vk = WithHash { data: vk, hash }; + + Some((account.id(), vk)) + }) + .collect() + } + pub fn to_all_verifiable( - ts: Vec>, - find_vk: F, - ) -> Result>, String> + ts: Vec>, + load_vk_cache: F, + ) -> Result>, String> where - F: Fn(Fp, &AccountId) -> Result, String>, - S: zkapp_command::Strategy, + S: zkapp_command::ToVerifiableStrategy, + F: Fn(HashSet) -> S::Cache, { - // https://github.com/MinaProtocol/mina/blob/436023ba41c43a50458a551b7ef7a9ae61670b25/src/lib/mina_base/user_command.ml#L180 - use itertools::Either; + let accounts_referenced: HashSet = ts + .iter() + .flat_map(|cmd| match cmd.cmd() { + UserCommand::SignedCommand(_) => Vec::new(), + UserCommand::ZkAppCommand(cmd) => cmd.accounts_referenced(), + }) + .collect(); + let mut vk_cache = load_vk_cache(accounts_referenced); + + ts.into_iter() + .map(|cmd| { + let is_failed = cmd.is_failed(); + let MaybeWithStatus { cmd, status } = cmd; + match cmd { + UserCommand::SignedCommand(c) => Ok(MaybeWithStatus { + cmd: verifiable::UserCommand::SignedCommand(c), + status, + }), + UserCommand::ZkAppCommand(c) => { + let zkapp_verifiable = S::create_all(&*c, is_failed, &mut vk_cache)?; + Ok(MaybeWithStatus { + cmd: verifiable::UserCommand::ZkAppCommand(Box::new(zkapp_verifiable)), + status, + }) + } + } + }) + .collect() + } - let (izk_cmds, is_cmds) = ts - .into_iter() - .enumerate() - .partition_map::, Vec<_>, _, _, _>(|(i, cmd)| match cmd.data { - UserCommand::ZkAppCommand(c) => Either::Left(( - i, - WithStatus { - data: c, - status: cmd.status, - }, - )), - UserCommand::SignedCommand(c) => Either::Right(( - i, - WithStatus { - data: c, - status: cmd.status, - }, - )), - }); + fn has_insufficient_fee(&self) -> bool { + /// `minimum_user_command_fee` + const MINIMUM_USER_COMMAND_FEE: Fee = Fee::from_u64(1000000); + self.fee() < MINIMUM_USER_COMMAND_FEE + } - // then unzip the indices - let (ixs, zk_cmds): (Vec<_>, Vec<_>) = izk_cmds.into_iter().unzip(); + fn has_zero_vesting_period(&self) -> bool { + match self { + UserCommand::SignedCommand(_cmd) => false, + UserCommand::ZkAppCommand(cmd) => cmd.has_zero_vesting_period(), + } + } - // then we verify the zkapp commands - let vzk_cmds = S::create_all(zk_cmds, find_vk)?; + fn is_incompatible_version(&self) -> bool { + match self { + UserCommand::SignedCommand(_cmd) => false, + UserCommand::ZkAppCommand(cmd) => cmd.is_incompatible_version(), + } + } - // rezip indices - let ivzk_cmds: Vec<_> = ixs.into_iter().zip(vzk_cmds).collect(); + fn is_disabled(&self) -> bool { + match self { + UserCommand::SignedCommand(_cmd) => false, + UserCommand::ZkAppCommand(_cmd) => false, // Mina_compile_config.zkapps_disabled + } + } - // Put them back in with a sort by index (un-partition) + fn valid_size(&self) -> Result<(), String> { + match self { + UserCommand::SignedCommand(_cmd) => Ok(()), + UserCommand::ZkAppCommand(cmd) => cmd.valid_size(), + } + } - use verifiable::UserCommand::{SignedCommand, ZkAppCommand}; - let mut ivs: Vec<_> = is_cmds - .into_iter() - .map(|(i, cmd)| (i, cmd.into_map(SignedCommand))) - .chain( - ivzk_cmds - .into_iter() - .map(|(i, cmd)| (i, cmd.into_map(|cmd| ZkAppCommand(Box::new(cmd))))), - ) - .collect(); + pub fn check_well_formedness(&self) -> Result<(), Vec> { + let mut errors: Vec<_> = [ + ( + Self::has_insufficient_fee as fn(_) -> _, + WellFormednessError::InsufficientFee, + ), + ( + Self::has_zero_vesting_period, + WellFormednessError::ZeroVestingPeriod, + ), + ( + Self::is_incompatible_version, + WellFormednessError::IncompatibleVersion, + ), + ( + Self::is_disabled, + WellFormednessError::TransactionTypeDisabled, + ), + ] + .iter() + .filter_map(|(fun, e)| if fun(self) { Some(e.clone()) } else { None }) + .collect(); - ivs.sort_unstable_by_key(|(i, _)| *i); + if let Err(e) = self.valid_size() { + errors.push(WellFormednessError::ZkappTooBig(e)); + } - // Drop the indices - Ok(ivs.into_iter().unzip::<_, _, Vec<_>, _>().1) + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } } } +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum WellFormednessError { + InsufficientFee, + ZeroVestingPeriod, + ZkappTooBig(String), + TransactionTypeDisabled, + IncompatibleVersion, +} + impl GenericCommand for UserCommand { fn fee(&self) -> Fee { match self { @@ -4361,7 +4806,7 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { } pub mod transaction_applied { - use crate::{Account, AccountId}; + use crate::AccountId; use super::*; @@ -4717,20 +5162,16 @@ pub mod protocol_state { pub mod local_state { use std::{cell::RefCell, rc::Rc}; - use ark_ff::Zero; - use crate::{ - hash_with_kimchi, proofs::{ field::{field, Boolean, ToBoolean}, numbers::nat::CheckedNat, to_field_elements::ToFieldElements, }, - scan_state::currency::{Index, Signed}, zkapps::intefaces::{ CallStackInterface, IndexInterface, SignedAmountInterface, StackFrameInterface, }, - Inputs, ToInputs, + ToInputs, }; use super::{zkapp_command::CallForest, *}; @@ -5542,14 +5983,14 @@ where { let perform = |eff: Eff| Env::perform(eff); - let original_account_states: Vec<_> = { + let original_account_states: Vec<(AccountId, Option<_>)> = { // get the original states of all the accounts in each pass. // If an account updated in the first pass is referenced in account // updates, then retain the value before first pass application*) let accounts_referenced = c.command.accounts_referenced(); - let mut account_states = BTreeMap::>::new(); + let mut account_states = BTreeMap::>::new(); let referenced = accounts_referenced.into_iter().map(|id| { let location = { @@ -5566,12 +6007,17 @@ where .for_each(|(id, acc_opt)| { use std::collections::btree_map::Entry::Vacant; - if let Vacant(entry) = account_states.entry(id) { + let id_with_order: AccountIdOrderable = id.into(); + if let Vacant(entry) = account_states.entry(id_with_order) { entry.insert(acc_opt); }; }); - account_states.into_iter().collect() + account_states + .into_iter() + // Convert back the `AccountIdOrder` into `AccountId`, now that they are sorted + .map(|(id, account): (AccountIdOrderable, Option<_>)| (id.into(), account)) + .collect() }; let mut account_states_after_fee_payer = { @@ -7605,15 +8051,12 @@ where #[cfg(test)] pub mod for_tests { - use std::collections::{HashMap, HashSet}; - use mina_signer::Keypair; use rand::Rng; - // use o1_utils::math::ceil_log2; use crate::{ - gen_keypair, scan_state::parallel_scan::ceil_log2, AuthRequired, BaseLedger, Mask, - Permissions, ZkAppAccount, TXN_VERSION_CURRENT, + gen_keypair, scan_state::parallel_scan::ceil_log2, AuthRequired, Mask, Permissions, + ZkAppAccount, TXN_VERSION_CURRENT, }; use super::*; diff --git a/ledger/src/staged_ledger/diff.rs b/ledger/src/staged_ledger/diff.rs index c641bdc13..9d3e03028 100644 --- a/ledger/src/staged_ledger/diff.rs +++ b/ledger/src/staged_ledger/diff.rs @@ -195,8 +195,6 @@ impl Diff { } pub mod with_valid_signatures_and_proofs { - use crate::scan_state::transaction_logic::valid; - use super::*; /// https://github.com/MinaProtocol/mina/blob/05c2f73d0f6e4f1341286843814ce02dcb3919e0/src/lib/staged_ledger_diff/diff_intf.ml#L123 @@ -304,7 +302,6 @@ pub mod with_valid_signatures_and_proofs { pub mod with_valid_signatures { use super::*; - use crate::scan_state::transaction_logic::valid; pub type PreDiffWithAtMostTwoCoinbase = PreDiffTwo>; diff --git a/ledger/src/staged_ledger/diff_creation_log.rs b/ledger/src/staged_ledger/diff_creation_log.rs index 2a03eca7d..ccc9c08b7 100644 --- a/ledger/src/staged_ledger/diff_creation_log.rs +++ b/ledger/src/staged_ledger/diff_creation_log.rs @@ -184,8 +184,6 @@ mod summary { } mod detail { - use crate::staged_ledger::diff::AtMostTwo; - use super::*; #[derive(Debug, Clone)] diff --git a/ledger/src/staged_ledger/staged_ledger.rs b/ledger/src/staged_ledger/staged_ledger.rs index efc8be9d0..01c41d2aa 100644 --- a/ledger/src/staged_ledger/staged_ledger.rs +++ b/ledger/src/staged_ledger/staged_ledger.rs @@ -27,8 +27,8 @@ use crate::{ apply_transaction_first_pass, apply_transaction_second_pass, local_state::LocalState, protocol_state::ProtocolStateView, transaction_partially_applied::TransactionPartiallyApplied, valid, - zkapp_command::verifiable::find_vk_via_ledger, CoinbaseFeeTransfer, Transaction, - TransactionStatus, UserCommand, WithStatus, + zkapp_command::MaybeWithStatus, CoinbaseFeeTransfer, Transaction, TransactionStatus, + UserCommand, WithStatus, }, }, sparse_ledger::{self, SparseLedger}, @@ -1205,13 +1205,20 @@ impl StagedLedger { cs: Vec>, skip_verification: Option, ) -> Result, VerifierError> { - use scan_state::transaction_logic::zkapp_command::last::Last; + use scan_state::transaction_logic::zkapp_command::from_applied_sequence::{ + self, FromAppliedSequence, + }; - let cs = - UserCommand::to_all_verifiable::, _>(cs, |expected_vk_hash, account_id| { - find_vk_via_ledger(ledger.clone(), expected_vk_hash, account_id) - }) - .unwrap(); // TODO: No unwrap + let cs = cs + .into_iter() + .map(MaybeWithStatus::from) + .collect::>(); + let cs = UserCommand::to_all_verifiable::(cs, |account_ids| { + let cache = UserCommand::load_vks_from_ledger(account_ids, &ledger); + from_applied_sequence::Cache::new(cache) + }) + .unwrap(); // TODO: No unwrap + let cs = cs.into_iter().map(WithStatus::from).collect::>(); verifier .verify_commands(cs, skip_verification) @@ -2031,7 +2038,7 @@ mod tests_ocaml { self, Common, PaymentPayload, SignedCommand, SignedCommandPayload, }, transaction_union_payload::TransactionUnionPayload, - zkapp_command::{self, SetOrKeep, WithHash}, + zkapp_command::{self, verifiable::find_vk_via_ledger, SetOrKeep, WithHash}, Memo, TransactionFailure, }, }, diff --git a/ledger/src/transaction_pool.rs b/ledger/src/transaction_pool.rs new file mode 100644 index 000000000..ff64db433 --- /dev/null +++ b/ledger/src/transaction_pool.rs @@ -0,0 +1,2106 @@ +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Borrow, + collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, + sync::Arc, +}; + +use itertools::Itertools; +use mina_hasher::Fp; +use mina_p2p_messages::{bigint::BigInt, v2}; +use openmina_core::constants::{ForkConstants, CONSTRAINT_CONSTANTS}; + +use crate::{ + scan_state::{ + currency::{Amount, Balance, BlockTime, Fee, Magnitude, Nonce, Slot, SlotSpan}, + fee_rate::FeeRate, + transaction_logic::{ + valid, + zkapp_command::{ + from_unapplied_sequence::{self, FromUnappliedSequence}, + MaybeWithStatus, WithHash, + }, + TransactionStatus::Applied, + UserCommand, WithStatus, + }, + }, + verifier::Verifier, + Account, AccountId, BaseLedger, Mask, TokenId, VerificationKey, +}; + +mod consensus { + use crate::scan_state::currency::{BlockTimeSpan, Epoch, Length}; + + use super::*; + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct Constants { + k: Length, + delta: Length, + slots_per_sub_window: Length, + slots_per_window: Length, + sub_windows_per_window: Length, + slots_per_epoch: Length, + grace_period_slots: Length, + grace_period_end: Slot, + checkpoint_window_slots_per_year: Length, + checkpoint_window_size_in_slots: Length, + block_window_duration_ms: BlockTimeSpan, + slot_duration_ms: BlockTimeSpan, + epoch_duration: BlockTimeSpan, + delta_duration: BlockTimeSpan, + genesis_state_timestamp: BlockTime, + } + + // Consensus epoch + impl Epoch { + fn of_time_exn(constants: &Constants, time: BlockTime) -> Result { + if time < constants.genesis_state_timestamp { + return Err( + "Epoch.of_time: time is earlier than genesis block timestamp".to_string(), + ); + } + + let time_since_genesis = time.diff(constants.genesis_state_timestamp); + let epoch = time_since_genesis.to_ms() / constants.epoch_duration.to_ms(); + let epoch: u32 = epoch.try_into().unwrap(); + + Ok(Self::from_u32(epoch)) + } + + fn start_time(constants: &Constants, epoch: Self) -> BlockTime { + let ms = constants + .genesis_state_timestamp + .to_span_since_epoch() + .to_ms() + + ((epoch.as_u32() as u64) * constants.epoch_duration.to_ms()); + BlockTime::of_span_since_epoch(BlockTimeSpan::of_ms(ms)) + } + + pub fn epoch_and_slot_of_time_exn( + constants: &Constants, + time: BlockTime, + ) -> Result<(Self, Slot), String> { + let epoch = Self::of_time_exn(constants, time)?; + let time_since_epoch = time.diff(Self::start_time(constants, epoch)); + + let slot: u64 = time_since_epoch.to_ms() / constants.slot_duration_ms.to_ms(); + let slot = Slot::from_u32(slot.try_into().unwrap()); + + Ok((epoch, slot)) + } + } + + /// TODO: Maybe rename to `ConsensusGlobalSlot` ? + pub struct GlobalSlot { + slot_number: Slot, + slots_per_epoch: Length, + } + + impl GlobalSlot { + fn create(constants: &Constants, epoch: Epoch, slot: Slot) -> Self { + let slot_number = slot.as_u32() + (constants.slots_per_epoch.as_u32() * epoch.as_u32()); + Self { + slot_number: Slot::from_u32(slot_number), + slots_per_epoch: constants.slots_per_epoch, + } + } + + fn of_epoch_and_slot(constants: &Constants, (epoch, slot): (Epoch, Slot)) -> Self { + Self::create(constants, epoch, slot) + } + + pub fn of_time_exn(constants: &Constants, time: BlockTime) -> Result { + Ok(Self::of_epoch_and_slot( + constants, + Epoch::epoch_and_slot_of_time_exn(constants, time)?, + )) + } + + pub fn to_global_slot(&self) -> Slot { + let Self { + slot_number, + slots_per_epoch: _, + } = self; + *slot_number + } + } +} + +/// Fee increase required to replace a transaction. +const REPLACE_FEE: Fee = Fee::of_nanomina_int_exn(1); + +type ValidCommandWithHash = WithHash; + +pub mod diff { + use super::*; + + #[derive(Debug, Clone)] + pub enum Error { + InsufficientReplaceFee, + Duplicate, + InvalidNonce, + InsufficientFunds, + Overflow, + BadToken, + UnwantedFeeToken, + Expired, + Overloaded, + FeePayerAccountNotFound, + FeePayerNotPermittedToSend, + AfterSlotTxEnd, + } + + impl Error { + pub fn grounds_for_diff_rejection(&self) -> bool { + match self { + Error::InsufficientReplaceFee + | Error::Duplicate + | Error::InvalidNonce + | Error::InsufficientFunds + | Error::Expired + | Error::Overloaded + | Error::FeePayerAccountNotFound + | Error::FeePayerNotPermittedToSend + | Error::AfterSlotTxEnd => false, + Error::Overflow | Error::BadToken | Error::UnwantedFeeToken => true, + } + } + } + + pub struct Diff { + pub list: Vec, + } + + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct DiffVerified { + pub list: Vec, + } + + struct Rejected { + list: Vec<(UserCommand, Error)>, + } + + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct BestTipDiff { + pub new_commands: Vec>, + pub removed_commands: Vec>, + pub reorg_best_tip: bool, + } +} + +fn preload_accounts( + ledger: &Mask, + account_ids: &BTreeSet, +) -> HashMap { + account_ids + .iter() + .filter_map(|id| { + let addr = ledger.location_of_account(id)?; + let account = ledger.get(addr)?; + Some((id.clone(), *account)) + }) + .collect() +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Config { + trust_system: (), + pool_max_size: usize, + slot_tx_end: Option, +} + +pub type VerificationKeyWire = WithHash; + +/// Used to be able to de/serialize our `TransactionPool` in the state machine +#[derive(Serialize, Deserialize)] +struct VkRefcountTableBigInts { + verification_keys: Vec<(BigInt, (usize, WithHash))>, + account_id_to_vks: Vec<(AccountId, Vec<(BigInt, usize)>)>, + vk_to_account_ids: Vec<(BigInt, Vec<(AccountId, usize)>)>, +} +impl From for VkRefcountTableBigInts { + fn from(value: VkRefcountTable) -> Self { + let VkRefcountTable { + verification_keys, + account_id_to_vks, + vk_to_account_ids, + } = value; + Self { + verification_keys: verification_keys + .into_iter() + .map(|(hash, (count, vk))| { + assert_eq!(hash, vk.hash); + let hash: BigInt = hash.into(); + ( + hash.clone(), + ( + count, + WithHash { + data: vk.data, + hash, + }, + ), + ) + }) + .collect(), + account_id_to_vks: account_id_to_vks + .into_iter() + .map(|(id, map)| { + ( + id, + map.into_iter() + .map(|(hash, count)| (hash.into(), count)) + .collect(), + ) + }) + .collect(), + vk_to_account_ids: vk_to_account_ids + .into_iter() + .map(|(hash, map)| (hash.into(), map.into_iter().collect())) + .collect(), + } + } +} +impl From for VkRefcountTable { + fn from(value: VkRefcountTableBigInts) -> Self { + let VkRefcountTableBigInts { + verification_keys, + account_id_to_vks, + vk_to_account_ids, + } = value; + Self { + verification_keys: verification_keys + .into_iter() + .map(|(hash, (count, vk))| { + assert_eq!(hash, vk.hash); + let hash: Fp = hash.to_field(); + ( + hash, + ( + count, + WithHash { + data: vk.data, + hash, + }, + ), + ) + }) + .collect(), + account_id_to_vks: account_id_to_vks + .into_iter() + .map(|(id, map)| { + let map = map + .into_iter() + .map(|(bigint, count)| (bigint.to_field::(), count)) + .collect(); + (id, map) + }) + .collect(), + vk_to_account_ids: vk_to_account_ids + .into_iter() + .map(|(hash, map)| (hash.to_field(), map.into_iter().collect())) + .collect(), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(into = "VkRefcountTableBigInts")] +#[serde(from = "VkRefcountTableBigInts")] +struct VkRefcountTable { + verification_keys: HashMap, + account_id_to_vks: HashMap>, + vk_to_account_ids: HashMap>, +} + +impl VkRefcountTable { + fn find_vk(&self, f: &Fp) -> Option<&(usize, WithHash)> { + self.verification_keys.get(f) + } + + fn find_vks_by_account_id(&self, account_id: &AccountId) -> Vec<&VerificationKeyWire> { + let Some(vks) = self.account_id_to_vks.get(account_id) else { + return Vec::new(); + }; + + vks.iter() + .map(|(f, _)| self.find_vk(f).expect("malformed Vk_refcount_table.t")) + .map(|(_, vk)| vk) + .collect() + } + + fn inc(&mut self, account_id: AccountId, vk: VerificationKeyWire) { + use std::collections::hash_map::Entry::{Occupied, Vacant}; + + match self.verification_keys.entry(vk.hash) { + Vacant(e) => { + e.insert((1, vk.clone())); + } + Occupied(mut e) => { + let (count, _vk) = e.get_mut(); + *count += 1; + } + } + + let map = self + .account_id_to_vks + .entry(account_id.clone()) + .or_default(); // or insert empty map + let count = map.entry(vk.hash).or_default(); // or insert count 0 + *count += 1; + + let map = self.vk_to_account_ids.entry(vk.hash).or_default(); // or insert empty map + let count = map.entry(account_id).or_default(); // or insert count 0 + *count += 1; + } + + fn dec(&mut self, account_id: AccountId, vk_hash: Fp) { + use std::collections::hash_map::Entry::{Occupied, Vacant}; + + match self.verification_keys.entry(vk_hash) { + Vacant(_e) => unreachable!(), + Occupied(mut e) => { + let (count, _vk) = e.get_mut(); + if *count == 1 { + e.remove(); + } else { + *count = (*count).checked_sub(1).unwrap(); + } + } + } + + fn remove(key1: K1, key2: K2, table: &mut HashMap>) + where + K1: std::hash::Hash + Eq, + K2: std::hash::Hash + Eq, + { + match table.entry(key1) { + Vacant(_e) => unreachable!(), + Occupied(mut e) => { + let map = e.get_mut(); + match map.entry(key2) { + Vacant(_e) => unreachable!(), + Occupied(mut e2) => { + let count: &mut usize = e2.get_mut(); + if *count == 1 { + e2.remove(); + e.remove(); + } else { + *count = count.checked_sub(1).expect("Invalid state"); + } + } + } + } + } + } + + remove(account_id.clone(), vk_hash, &mut self.account_id_to_vks); + remove(vk_hash, account_id.clone(), &mut self.vk_to_account_ids); + } + + fn decrement_list(&mut self, list: &[WithStatus]) { + list.iter().for_each(|c| { + for (id, vk) in c.data.forget_check().extract_vks() { + self.dec(id, vk.hash); + } + }); + } + + fn decrement_hashed(&mut self, list: I) + where + I: IntoIterator, + V: Borrow, + { + list.into_iter().for_each(|c| { + for (id, vk) in c.borrow().data.forget_check().extract_vks() { + self.dec(id, vk.hash); + } + }); + } + + fn increment_hashed(&mut self, list: I) + where + I: IntoIterator, + V: Borrow, + { + list.into_iter().for_each(|c| { + for (id, vk) in c.borrow().data.forget_check().extract_vks() { + self.inc(id, vk); + } + }); + } + + fn increment_list(&mut self, list: &[WithStatus]) { + list.iter().for_each(|c| { + for (id, vk) in c.data.forget_check().extract_vks() { + self.inc(id, vk); + } + }); + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +enum Batch { + Of(usize), +} + +pub enum CommandError { + InvalidNonce { + account_nonce: Nonce, + expected: Nonce, + }, + InsufficientFunds { + balance: Balance, + consumed: Amount, + }, + /// NOTE: don't punish for this, attackers can induce nodes to banlist + /// each other that way! *) + InsufficientReplaceFee { + replace_fee: Fee, + fee: Fee, + }, + Overflow, + BadToken, + Expired { + valid_until: Slot, + global_slot_since_genesis: Slot, + }, + UnwantedFeeToken { + token_id: TokenId, + }, + AfterSlotTxEnd, +} + +impl From for diff::Error { + fn from(value: CommandError) -> Self { + match value { + CommandError::InvalidNonce { .. } => diff::Error::InvalidNonce, + CommandError::InsufficientFunds { .. } => diff::Error::InsufficientFunds, + CommandError::InsufficientReplaceFee { .. } => diff::Error::InsufficientReplaceFee, + CommandError::Overflow => diff::Error::Overflow, + CommandError::BadToken => diff::Error::BadToken, + CommandError::Expired { .. } => diff::Error::Expired, + CommandError::UnwantedFeeToken { .. } => diff::Error::UnwantedFeeToken, + CommandError::AfterSlotTxEnd => diff::Error::AfterSlotTxEnd, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct IndexedPoolConfig { + consensus_constants: consensus::Constants, + slot_tx_end: Option, +} + +// module Config = struct +// type t = +// { constraint_constants : Genesis_constants.Constraint_constants.t +// ; consensus_constants : Consensus.Constants.t +// ; time_controller : Block_time.Controller.t +// ; slot_tx_end : Mina_numbers.Global_slot_since_hard_fork.t option +// } +// [@@deriving sexp_of, equal, compare] +// end + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct IndexedPool { + /// Transactions valid against the current ledger, indexed by fee per + /// weight unit. + applicable_by_fee: HashMap>, + /// All pending transactions along with the total currency required to + /// execute them -- plus any currency spent from this account by + /// transactions from other accounts -- indexed by sender account. + /// Ordered by nonce inside the accounts. + all_by_sender: HashMap, Amount)>, + /// All transactions in the pool indexed by fee per weight unit. + all_by_fee: HashMap>, + all_by_hash: HashMap, + /// Only transactions that have an expiry + transactions_with_expiration: HashMap>, + size: usize, + config: IndexedPoolConfig, +} + +enum Update { + Add { + command: ValidCommandWithHash, + fee_per_wu: FeeRate, + add_to_applicable_by_fee: bool, + }, + RemoveAllByFeeAndHashAndExpiration { + commands: VecDeque, + }, + RemoveFromApplicableByFee { + fee_per_wu: FeeRate, + command: ValidCommandWithHash, + }, +} + +#[derive(Clone)] +struct SenderState { + sender: AccountId, + state: Option<(VecDeque, Amount)>, +} + +pub enum RevalidateKind<'a> { + EntirePool, + Subset(&'a BTreeMap), +} + +impl IndexedPool { + fn size(&self) -> usize { + self.size + } + + fn min_fee(&self) -> Option { + self.all_by_fee.keys().min().cloned() + } + + fn member(&self, cmd: &ValidCommandWithHash) -> bool { + self.all_by_hash.contains_key(&cmd.hash) + } + + fn global_slot_since_genesis(&self) -> Slot { + let current_time = BlockTime::now(); + + let current_slot = + consensus::GlobalSlot::of_time_exn(&self.config.consensus_constants, current_time) + .unwrap() + .to_global_slot(); + + match CONSTRAINT_CONSTANTS.fork.as_ref() { + Some(ForkConstants { genesis_slot, .. }) => { + let genesis_slot = Slot::from_u32(*genesis_slot); + let slot_span = SlotSpan::from_u32(current_slot.as_u32()); + genesis_slot.add(slot_span) + } + None => current_slot, + } + } + + fn check_expiry(&self, cmd: &UserCommand) -> Result<(), CommandError> { + let global_slot_since_genesis = self.global_slot_since_genesis(); + let valid_until = cmd.valid_until(); + + if valid_until < global_slot_since_genesis { + return Err(CommandError::Expired { + valid_until, + global_slot_since_genesis, + }); + } + + Ok(()) + } + + /// Insert in a `HashMap<_, HashSet<_>>` + fn map_set_insert(map: &mut HashMap>, key: K, value: V) + where + K: std::hash::Hash + PartialEq + Eq, + V: std::hash::Hash + PartialEq + Eq, + { + let entry = map.entry(key).or_default(); + entry.insert(value); + } + + /// Remove in a `HashMap<_, HashSet<_>>` + fn map_set_remove(map: &mut HashMap>, key: K, value: &V) + where + K: std::hash::Hash + PartialEq + Eq, + V: std::hash::Hash + PartialEq + Eq, + { + let Entry::Occupied(mut entry) = map.entry(key) else { + return; + }; + let set = entry.get_mut(); + set.remove(value); + if set.is_empty() { + entry.remove(); + } + } + + fn update_expiration_map(&mut self, cmd: ValidCommandWithHash, is_add: bool) { + let user_cmd = cmd.data.forget_check(); + let expiry = user_cmd.valid_until(); + if expiry == Slot::max() { + return; // Do nothing + } + if is_add { + Self::map_set_insert(&mut self.transactions_with_expiration, expiry, cmd); + } else { + Self::map_set_remove(&mut self.transactions_with_expiration, expiry, &cmd); + } + } + + fn remove_from_expiration_exn(&mut self, cmd: ValidCommandWithHash) { + self.update_expiration_map(cmd, false); + } + + fn add_to_expiration(&mut self, cmd: ValidCommandWithHash) { + self.update_expiration_map(cmd, true); + } + + /// Remove a command from the applicable_by_fee field. This may break an + /// invariant. + fn remove_applicable_exn(&mut self, cmd: &ValidCommandWithHash) { + let fee_per_wu = cmd.data.forget_check().fee_per_wu(); + Self::map_set_remove(&mut self.applicable_by_fee, fee_per_wu, cmd); + } + + fn make_queue() -> VecDeque { + VecDeque::with_capacity(256) + } + + fn add_from_backtrack(&mut self, cmd: ValidCommandWithHash) -> Result<(), CommandError> { + let IndexedPoolConfig { slot_tx_end, .. } = &self.config; + + let current_global_slot = self.current_global_slot(); + + if !slot_tx_end + .as_ref() + .map(|slot_tx_end| current_global_slot < *slot_tx_end) + .unwrap_or(true) + { + return Err(CommandError::AfterSlotTxEnd); + } + + let ValidCommandWithHash { + data: unchecked, + hash: cmd_hash, + } = &cmd; + let unchecked = unchecked.forget_check(); + + self.check_expiry(&unchecked)?; + + let fee_payer = unchecked.fee_payer(); + let fee_per_wu = unchecked.fee_per_wu(); + + let consumed = currency_consumed(&unchecked).unwrap(); + + match self.all_by_sender.get_mut(&fee_payer) { + None => { + { + let mut queue = Self::make_queue(); + queue.push_back(cmd.clone()); + self.all_by_sender.insert(fee_payer, (queue, consumed)); + } + Self::map_set_insert(&mut self.all_by_fee, fee_per_wu.clone(), cmd.clone()); + self.all_by_hash.insert(cmd_hash.clone(), cmd.clone()); + Self::map_set_insert(&mut self.applicable_by_fee, fee_per_wu.clone(), cmd.clone()); + self.add_to_expiration(cmd); + self.size += 1; + } + Some((queue, currency_reserved)) => { + let first_queued = queue.front().cloned().unwrap(); + + if unchecked.expected_target_nonce() + != first_queued.data.forget_check().applicable_at_nonce() + { + panic!("indexed pool nonces inconsistent when adding from backtrack.") + } + + // update `self.all_by_sender` + { + queue.push_front(cmd.clone()); + *currency_reserved = currency_reserved.checked_add(&consumed).unwrap(); + } + + self.remove_applicable_exn(&first_queued); + + Self::map_set_insert(&mut self.applicable_by_fee, fee_per_wu.clone(), cmd.clone()); + Self::map_set_insert(&mut self.all_by_fee, fee_per_wu.clone(), cmd.clone()); + self.all_by_hash.insert(cmd_hash.clone(), cmd.clone()); + self.add_to_expiration(cmd); + self.size += 1; + } + } + Ok(()) + } + + fn current_global_slot(&self) -> Slot { + let IndexedPoolConfig { + consensus_constants, + slot_tx_end: _, + } = &self.config; + + consensus::GlobalSlot::of_time_exn(consensus_constants, BlockTime::now()) + .unwrap() + .to_global_slot() + } + + fn update_add( + &mut self, + cmd: ValidCommandWithHash, + fee_per_wu: FeeRate, + add_to_applicable_by_fee: bool, + ) { + if add_to_applicable_by_fee { + Self::map_set_insert(&mut self.applicable_by_fee, fee_per_wu.clone(), cmd.clone()); + } + + let cmd_hash = cmd.hash.clone(); + + Self::map_set_insert(&mut self.all_by_fee, fee_per_wu, cmd.clone()); + self.all_by_hash.insert(cmd_hash, cmd.clone()); + self.add_to_expiration(cmd); + self.size += 1; + } + + /// Remove a command from the all_by_fee and all_by_hash fields, and decrement + /// size. This may break an invariant. + fn update_remove_all_by_fee_and_hash_and_expiration(&mut self, cmds: I) + where + I: IntoIterator, + { + for cmd in cmds { + let fee_per_wu = cmd.data.forget_check().fee_per_wu(); + let cmd_hash = cmd.hash.clone(); + Self::map_set_remove(&mut self.all_by_fee, fee_per_wu, &cmd); + self.all_by_hash.remove(&cmd_hash); + self.remove_from_expiration_exn(cmd); + self.size = self.size.checked_sub(1).unwrap(); + } + } + + fn update_remove_from_applicable_by_fee( + &mut self, + fee_per_wu: FeeRate, + command: &ValidCommandWithHash, + ) { + Self::map_set_remove(&mut self.applicable_by_fee, fee_per_wu, command) + } + + fn remove_with_dependents_exn( + &mut self, + cmd: &ValidCommandWithHash, + ) -> VecDeque { + let sender = cmd.data.fee_payer(); + let mut by_sender = SenderState { + state: self.all_by_sender.get(&sender).cloned(), + sender, + }; + + let mut updates = Vec::::with_capacity(128); + let result = self.remove_with_dependents_exn_impl(cmd, &mut by_sender, &mut updates); + + self.set_sender(by_sender); + self.apply_updates(updates); + + result + } + + fn remove_with_dependents_exn_impl( + &self, + cmd: &ValidCommandWithHash, + by_sender: &mut SenderState, + updates: &mut Vec, + ) -> VecDeque { + let (sender_queue, reserved_currency_ref) = by_sender.state.as_mut().unwrap(); + let unchecked = cmd.data.forget_check(); + + assert!(!sender_queue.is_empty()); + + let cmd_nonce = unchecked.applicable_at_nonce(); + + let cmd_index = sender_queue + .iter() + .position(|cmd| { + let nonce = cmd.data.forget_check().applicable_at_nonce(); + // we just compare nonce equality since the command we are looking for already exists in the sequence + nonce == cmd_nonce + }) + .unwrap(); + + let drop_queue = sender_queue.split_off(cmd_index); + let keep_queue = sender_queue; + assert!(!drop_queue.is_empty()); + + let currency_to_remove = drop_queue.iter().fold(Amount::zero(), |acc, cmd| { + let consumed = currency_consumed(&cmd.data.forget_check()).unwrap(); + consumed.checked_add(&acc).unwrap() + }); + + // This is safe because the currency in a subset of the commands much be <= + // total currency in all the commands. + let reserved_currency = reserved_currency_ref + .checked_sub(¤cy_to_remove) + .unwrap(); + + updates.push(Update::RemoveAllByFeeAndHashAndExpiration { + commands: drop_queue.clone(), + }); + + if cmd_index == 0 { + updates.push(Update::RemoveFromApplicableByFee { + fee_per_wu: unchecked.fee_per_wu(), + command: cmd.clone(), + }); + } + + // We re-fetch it to make the borrow checker happy + // let (keep_queue, reserved_currency_ref) = self.all_by_sender.get_mut(&sender).unwrap(); + if !keep_queue.is_empty() { + *reserved_currency_ref = reserved_currency; + } else { + assert!(reserved_currency.is_zero()); + by_sender.state = None; + } + + drop_queue + } + + fn apply_updates(&mut self, updates: Vec) { + for update in updates { + match update { + Update::Add { + command, + fee_per_wu, + add_to_applicable_by_fee, + } => self.update_add(command, fee_per_wu, add_to_applicable_by_fee), + Update::RemoveAllByFeeAndHashAndExpiration { commands } => { + self.update_remove_all_by_fee_and_hash_and_expiration(commands) + } + Update::RemoveFromApplicableByFee { + fee_per_wu, + command, + } => self.update_remove_from_applicable_by_fee(fee_per_wu, &command), + } + } + } + + fn set_sender(&mut self, by_sender: SenderState) { + let SenderState { sender, state } = by_sender; + + match state { + Some(state) => { + self.all_by_sender.insert(sender, state); + } + None => { + self.all_by_sender.remove(&sender); + } + } + } + + fn add_from_gossip_exn( + &mut self, + cmd: &ValidCommandWithHash, + current_nonce: Nonce, + balance: Balance, + ) -> Result<(ValidCommandWithHash, VecDeque), CommandError> { + let sender = cmd.data.fee_payer(); + let mut by_sender = SenderState { + state: self.all_by_sender.get(&sender).cloned(), + sender, + }; + + let mut updates = Vec::::with_capacity(128); + let result = self.add_from_gossip_exn_impl( + cmd, + current_nonce, + balance, + &mut by_sender, + &mut updates, + )?; + + self.set_sender(by_sender); + self.apply_updates(updates); + + Ok(result) + } + + fn add_from_gossip_exn_impl( + &self, + cmd: &ValidCommandWithHash, + current_nonce: Nonce, + balance: Balance, + by_sender: &mut SenderState, + updates: &mut Vec, + ) -> Result<(ValidCommandWithHash, VecDeque), CommandError> { + let IndexedPoolConfig { slot_tx_end, .. } = &self.config; + + let current_global_slot = self.current_global_slot(); + + if !slot_tx_end + .as_ref() + .map(|slot_tx_end| current_global_slot < *slot_tx_end) + .unwrap_or(true) + { + return Err(CommandError::AfterSlotTxEnd); + } + + let unchecked = cmd.data.forget_check(); + let fee = unchecked.fee(); + let fee_per_wu = unchecked.fee_per_wu(); + let cmd_applicable_at_nonce = unchecked.applicable_at_nonce(); + + let consumed = { + self.check_expiry(&unchecked)?; + let consumed = currency_consumed(&unchecked).ok_or(CommandError::Overflow)?; + if !unchecked.fee_token().is_default() { + return Err(CommandError::BadToken); + } + consumed + }; + + match by_sender.state.as_mut() { + None => { + if current_nonce != cmd_applicable_at_nonce { + return Err(CommandError::InvalidNonce { + account_nonce: current_nonce, + expected: cmd_applicable_at_nonce, + }); + } + if consumed > balance.to_amount() { + return Err(CommandError::InsufficientFunds { balance, consumed }); + } + + let mut queue = Self::make_queue(); + queue.push_back(cmd.clone()); + by_sender.state = Some((queue, consumed)); + + updates.push(Update::Add { + command: cmd.clone(), + fee_per_wu, + add_to_applicable_by_fee: true, + }); + + Ok((cmd.clone(), Self::make_queue())) + } + Some((queued_cmds, reserved_currency)) => { + assert!(!queued_cmds.is_empty()); + let queue_applicable_at_nonce = { + let first = queued_cmds.front().unwrap(); + first.data.forget_check().applicable_at_nonce() + }; + let queue_target_nonce = { + let last = queued_cmds.back().unwrap(); + last.data.forget_check().expected_target_nonce() + }; + if queue_target_nonce == cmd_applicable_at_nonce { + let reserved_currency = consumed + .checked_add(reserved_currency) + .ok_or(CommandError::Overflow)?; + + if !(reserved_currency <= balance.to_amount()) { + return Err(CommandError::InsufficientFunds { + balance, + consumed: reserved_currency, + }); + } + + queued_cmds.push_back(cmd.clone()); + + updates.push(Update::Add { + command: cmd.clone(), + fee_per_wu, + add_to_applicable_by_fee: false, + }); + + Ok((cmd.clone(), Self::make_queue())) + } else if queue_applicable_at_nonce == current_nonce { + if !cmd_applicable_at_nonce + .between(&queue_applicable_at_nonce, &queue_target_nonce) + { + return Err(CommandError::InvalidNonce { + account_nonce: cmd_applicable_at_nonce, + expected: queue_applicable_at_nonce, + }); + } + + let replacement_index = queued_cmds + .iter() + .position(|cmd| { + let cmd_applicable_at_nonce_prime = + cmd.data.forget_check().applicable_at_nonce(); + cmd_applicable_at_nonce <= cmd_applicable_at_nonce_prime + }) + .unwrap(); + + let drop_queue = queued_cmds.split_off(replacement_index); + + let to_drop = drop_queue.front().unwrap().data.forget_check(); + assert!(cmd_applicable_at_nonce <= to_drop.applicable_at_nonce()); + + // We check the fee increase twice because we need to be sure the + // subtraction is safe. + { + let replace_fee = to_drop.fee(); + if !(fee >= replace_fee) { + return Err(CommandError::InsufficientReplaceFee { replace_fee, fee }); + } + } + + let dropped = self.remove_with_dependents_exn_impl( + drop_queue.front().unwrap(), + by_sender, + updates, + ); + assert_eq!(drop_queue, dropped); + + let (cmd, _) = { + let (v, dropped) = self.add_from_gossip_exn_impl( + cmd, + current_nonce, + balance, + by_sender, + updates, + )?; + // We've already removed them, so this should always be empty. + assert!(dropped.is_empty()); + (v, dropped) + }; + + let drop_head = dropped.front().cloned().unwrap(); + let mut drop_tail = dropped.into_iter().skip(1).peekable(); + + let mut increment = fee.checked_sub(&to_drop.fee()).unwrap(); + let mut dropped = None::>; + let mut current_nonce = current_nonce; + let mut this_updates = Vec::with_capacity(128); + + while let Some(cmd) = drop_tail.peek() { + if dropped.is_some() { + let cmd_unchecked = cmd.data.forget_check(); + let replace_fee = cmd_unchecked.fee(); + + increment = increment.checked_sub(&replace_fee).ok_or_else(|| { + CommandError::InsufficientReplaceFee { + replace_fee, + fee: increment, + } + })?; + } else { + current_nonce = current_nonce.succ(); + let by_sender_pre = by_sender.clone(); + this_updates.clear(); + + match self.add_from_gossip_exn_impl( + cmd, + current_nonce, + balance, + by_sender, + &mut this_updates, + ) { + Ok((_cmd, dropped)) => { + assert!(dropped.is_empty()); + updates.append(&mut this_updates); + } + Err(_) => { + *by_sender = by_sender_pre; + dropped = Some(drop_tail.clone().skip(1).collect()); + continue; // Don't go to next + } + } + } + let _ = drop_tail.next(); + } + + if !(increment >= REPLACE_FEE) { + return Err(CommandError::InsufficientReplaceFee { + replace_fee: REPLACE_FEE, + fee: increment, + }); + } + + let mut dropped = dropped.unwrap_or_else(Self::make_queue); + dropped.push_front(drop_head); + + Ok((cmd, dropped)) + } else { + // Invalid nonce or duplicate transaction got in- either way error + Err(CommandError::InvalidNonce { + account_nonce: cmd_applicable_at_nonce, + expected: queue_target_nonce, + }) + } + } + } + } + + fn expired_by_global_slot(&self) -> Vec { + let global_slot_since_genesis = self.global_slot_since_genesis(); + + self.transactions_with_expiration + .iter() + .filter(|(slot, _cmd)| **slot < global_slot_since_genesis) + .flat_map(|(_slot, cmd)| cmd.iter().cloned()) + .collect() + } + + fn expired(&self) -> Vec { + self.expired_by_global_slot() + } + + fn remove_expired(&mut self) -> Vec { + let mut dropped = Vec::with_capacity(128); + for cmd in self.expired() { + if self.member(&cmd) { + let removed = self.remove_with_dependents_exn(&cmd); + dropped.extend(removed); + } + } + dropped + } + + fn remove_lowest_fee(&mut self) -> VecDeque { + let Some(set) = self.min_fee().and_then(|fee| self.all_by_fee.get(&fee)) else { + return VecDeque::new(); + }; + + // TODO: Should `self.all_by_fee` be a `BTreeSet` instead ? + let bset: BTreeSet<_> = set.iter().collect(); + // TODO: Not sure if OCaml compare the same way than we do + let min = bset.first().map(|min| (*min).clone()).unwrap(); + + self.remove_with_dependents_exn(&min) + } + + /// Drop commands from the end of the queue until the total currency consumed is + /// <= the current balance. + fn drop_until_sufficient_balance( + mut queue: VecDeque, + mut currency_reserved: Amount, + current_balance: Amount, + ) -> ( + VecDeque, + Amount, + VecDeque, + ) { + let mut dropped_so_far = VecDeque::with_capacity(queue.len()); + + while currency_reserved > current_balance { + let last = queue.pop_back().unwrap(); + let consumed = currency_consumed(&last.data.forget_check()).unwrap(); + dropped_so_far.push_back(last); + currency_reserved = currency_reserved.checked_sub(&consumed).unwrap(); + } + + (queue, currency_reserved, dropped_so_far) + } + + fn revalidate(&mut self, kind: RevalidateKind, get_account: F) -> Vec + where + F: Fn(&AccountId) -> Account, + { + let requires_revalidation = |account_id: &AccountId| match kind { + RevalidateKind::EntirePool => true, + RevalidateKind::Subset(set) => set.contains_key(account_id), + }; + + let mut dropped = Vec::new(); + + for (sender, (mut queue, mut currency_reserved)) in self.all_by_sender.clone() { + if !requires_revalidation(&sender) { + continue; + } + let account: Account = get_account(&sender); + let current_balance = { + let global_slot = self.global_slot_since_genesis(); + account.liquid_balance_at_slot(global_slot).to_amount() + }; + let first_cmd = queue.front().unwrap(); + let first_nonce = first_cmd.data.forget_check().applicable_at_nonce(); + + if !(account.has_permission_to_send() && account.has_permission_to_increment_nonce()) { + let this_dropped = self.remove_with_dependents_exn(first_cmd); + dropped.extend(this_dropped); + } else if account.nonce < first_nonce { + let this_dropped = self.remove_with_dependents_exn(first_cmd); + dropped.extend(this_dropped); + } else { + // current_nonce >= first_nonce + let first_applicable_nonce_index = queue.iter().position(|cmd| { + let nonce = cmd.data.forget_check().applicable_at_nonce(); + nonce == account.nonce + }); + + let keep_queue = match first_applicable_nonce_index { + Some(index) => queue.split_off(index), + None => Default::default(), + }; + let drop_queue = queue; + + for cmd in &drop_queue { + currency_reserved = currency_reserved + .checked_sub(¤cy_consumed(&cmd.data.forget_check()).unwrap()) + .unwrap(); + } + + let (keep_queue, currency_reserved, dropped_for_balance) = + Self::drop_until_sufficient_balance( + keep_queue, + currency_reserved, + current_balance, + ); + + let to_drop: Vec<_> = drop_queue.into_iter().chain(dropped_for_balance).collect(); + + let Some(head) = to_drop.first() else { + continue; + }; + + self.remove_applicable_exn(head); + self.update_remove_all_by_fee_and_hash_and_expiration(to_drop.clone()); + + match keep_queue.front().cloned() { + None => { + self.all_by_sender.remove(&sender); + } + Some(first_kept) => { + let first_kept_unchecked = first_kept.data.forget_check(); + self.all_by_sender + .insert(sender, (keep_queue, currency_reserved)); + Self::map_set_insert( + &mut self.applicable_by_fee, + first_kept_unchecked.fee_per_wu(), + first_kept, + ); + } + } + + dropped.extend(to_drop); + } + } + + dropped + } + + /// Returns a sequence of commands in the pool in descending fee order + fn transactions(&mut self) -> Vec { + let mut txns = Vec::with_capacity(self.applicable_by_fee.len()); + loop { + if self.applicable_by_fee.is_empty() { + assert!(self.all_by_sender.is_empty()); + return txns; + } + + let (fee, mut set) = self + .applicable_by_fee + .iter() + .max_by_key(|(rate, _)| *rate) + .map(|(rate, set)| (rate.clone(), set.clone())) + .unwrap(); + + // TODO: Check if OCaml compare using `hash` (order) + let txn = set.iter().min_by_key(|b| &*b.hash).cloned().unwrap(); + + { + set.remove(&txn); + if set.is_empty() { + self.applicable_by_fee.remove(&fee); + } else { + self.applicable_by_fee.insert(fee, set); + } + } + + let sender = txn.data.forget_check().fee_payer(); + + let (sender_queue, _amount) = self.all_by_sender.get_mut(&sender).unwrap(); + let head_txn = sender_queue.pop_front().unwrap(); + + if txn.hash == head_txn.hash { + match sender_queue.front().cloned() { + None => { + self.all_by_sender.remove(&sender); + } + Some(next_txn) => { + let fee = next_txn.data.forget_check().fee_per_wu(); + self.applicable_by_fee + .entry(fee) + .or_default() + .insert(next_txn); + } + } + } else { + eprintln!("Sender queue is malformed"); + self.all_by_sender.remove(&sender); + } + + txns.push(txn); + } + } +} + +fn currency_consumed(cmd: &UserCommand) -> Option { + use crate::scan_state::transaction_logic::signed_command::{Body::*, PaymentPayload}; + + let fee_amount = Amount::of_fee(&cmd.fee()); + let amount = match cmd { + UserCommand::SignedCommand(c) => { + match &c.payload.body { + Payment(PaymentPayload { amount, .. }) => { + // The fee-payer is also the sender account, include the amount. + *amount + } + StakeDelegation(_) => Amount::zero(), + } + } + UserCommand::ZkAppCommand(_) => Amount::zero(), + }; + + fee_amount.checked_add(&amount) +} + +type BlakeHash = Arc<[u8; 32]>; + +mod transaction_hash { + use blake2::{ + digest::{Update, VariableOutput}, + Blake2bVar, + }; + use mina_signer::Signature; + + use crate::scan_state::transaction_logic::{ + signed_command::SignedCommand, zkapp_command::AccountUpdate, + }; + + use super::*; + + pub fn hash_command(cmd: valid::UserCommand) -> ValidCommandWithHash { + use mina_p2p_messages::binprot::BinProtWrite; + + fn to_binprot, V: BinProtWrite>(v: T) -> Vec { + let value = v.into(); + let mut buffer = Vec::with_capacity(32 * 1024); + value.binprot_write(&mut buffer).unwrap(); + buffer + } + + let buffer: Vec = match &cmd { + valid::UserCommand::SignedCommand(cmd) => { + let mut cmd: SignedCommand = (**cmd).clone(); + cmd.signature = Signature::dummy(); + to_binprot::<_, v2::MinaBaseSignedCommandStableV2>(&cmd) + } + valid::UserCommand::ZkAppCommand(cmd) => { + let mut cmd = cmd.clone().forget(); + cmd.fee_payer.authorization = Signature::dummy(); + cmd.account_updates = cmd.account_updates.map_to(|account_update| { + let dummy_auth = account_update.authorization.dummy(); + AccountUpdate { + authorization: dummy_auth, + ..account_update.clone() + } + }); + to_binprot::<_, v2::MinaBaseZkappCommandTStableV1WireStableV1>(&cmd) + } + }; + + let mut hasher = Blake2bVar::new(32).expect("Invalid Blake2bVar output size"); + hasher.update(&buffer); + + let hash: Arc<[u8; 32]> = { + let mut buffer = [0; 32]; + hasher + .finalize_variable(&mut buffer) + .expect("Invalid buffer size"); // Never occur + Arc::from(buffer) + }; + + WithHash { data: cmd, hash } + } +} + +// TODO: Remove this +pub struct Envelope { + pub data: T, +} + +impl Envelope { + fn data(&self) -> &T { + &self.data + } + + fn is_sender_local(&self) -> bool { + todo!() + } +} + +#[derive(Debug)] +pub enum ApplyDecision { + Accept, + Reject, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Time { + nanoseconds_since_unix_epoch: u64, +} + +impl Time { + fn now() -> Self { + const NANOS_PER_SECOND: u64 = 1000000000; + + let mut tp = libc::timeval { + tv_sec: 0, + tv_usec: 0, + }; + + let result = unsafe { + // Use same syscall than OCaml: + // https://github.com/janestreet/time_now/blob/d7e3801d2f120b6723c28429de0dd63b669d47b8/src/time_now_stubs.c#L30 + libc::gettimeofday(&mut tp, std::ptr::null_mut()) + }; + if result == -1 { + return Self { + nanoseconds_since_unix_epoch: 0, + }; + } + + Self { + nanoseconds_since_unix_epoch: NANOS_PER_SECOND + .wrapping_mul(tp.tv_sec as u64) + .wrapping_add((tp.tv_usec as u64).wrapping_mul(1000)), + } + } +} + +const MAX_PER_15_SECONDS: usize = 10; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransactionPool { + pool: IndexedPool, + locally_generated_uncommitted: HashMap, + locally_generated_committed: HashMap, + current_batch: usize, + remaining_in_batch: usize, + config: Config, + batcher: (), + best_tip_diff_relay: Option<()>, + verification_key_table: VkRefcountTable, +} + +impl TransactionPool { + pub fn new() -> Self { + Self { + pool: todo!(), + locally_generated_uncommitted: todo!(), + locally_generated_committed: todo!(), + current_batch: todo!(), + remaining_in_batch: todo!(), + config: todo!(), + batcher: todo!(), + best_tip_diff_relay: todo!(), + verification_key_table: todo!(), + } + } + + pub fn transactions(&mut self) -> Vec { + self.pool.transactions() + } + + pub fn get_accounts_to_revalidate_on_new_best_tip(&self) -> BTreeSet { + self.pool.all_by_sender.keys().cloned().collect() + } + + pub fn on_new_best_tip(&mut self, accounts: &BTreeMap) { + let dropped = self + .pool + .revalidate(RevalidateKind::EntirePool, |sender_id| { + accounts + .get(sender_id) + .cloned() + .unwrap_or_else(Account::empty) + }); + + let dropped_locally_generated = dropped + .iter() + .filter(|cmd| { + let dropped_commited = self.locally_generated_committed.remove(cmd).is_some(); + let dropped_uncommited = self.locally_generated_uncommitted.remove(cmd).is_some(); + // Nothing should be in both tables. + assert!(!(dropped_commited && dropped_uncommited)); + dropped_commited || dropped_uncommited + }) + .collect::>(); + + if !dropped_locally_generated.is_empty() { + eprintln!( + "Dropped locally generated commands $cmds from pool when transition frontier was recreated. {:?}", + dropped_locally_generated + ) + } + } + + fn has_sufficient_fee(&self, pool_max_size: usize, cmd: &valid::UserCommand) -> bool { + match self.pool.min_fee() { + None => true, + Some(min_fee) => { + if self.pool.size() >= pool_max_size { + cmd.forget_check().fee_per_wu() > min_fee + } else { + true + } + } + } + } + + fn drop_until_below_max_size(&mut self, pool_max_size: usize) -> Vec { + let mut list = Vec::new(); + + while self.pool.size() > pool_max_size { + let dropped = self.pool.remove_lowest_fee(); + assert!(!dropped.is_empty()); + list.extend(dropped) + } + + list + } + + pub fn get_accounts_to_handle_transition_diff( + &self, + diff: &diff::BestTipDiff, + ) -> BTreeSet { + let diff::BestTipDiff { + new_commands, + removed_commands, + reorg_best_tip: _, + } = diff; + + new_commands + .iter() + .chain(removed_commands) + .flat_map(|cmd| cmd.data.forget_check().accounts_referenced()) + .chain( + self.locally_generated_uncommitted + .keys() + .map(|cmd| cmd.data.fee_payer()), + ) + .collect::>() + } + + pub fn handle_transition_frontier_diff( + &mut self, + diff: &diff::BestTipDiff, + accounts: &BTreeMap, + ) { + let diff::BestTipDiff { + new_commands, + removed_commands, + reorg_best_tip: _, + } = diff; + + let global_slot = self.pool.global_slot_since_genesis(); + + let pool_max_size = self.config.pool_max_size; + + self.verification_key_table.increment_list(new_commands); + self.verification_key_table.decrement_list(removed_commands); + + let mut dropped_backtrack = Vec::with_capacity(256); + for cmd in removed_commands { + let cmd = transaction_hash::hash_command(cmd.data.clone()); + + if let Some(time_added) = self.locally_generated_committed.remove(&cmd) { + self.locally_generated_uncommitted + .insert(cmd.clone(), time_added); + } + + let dropped_seq = match self.pool.add_from_backtrack(cmd) { + Ok(_) => self.drop_until_below_max_size(pool_max_size), + Err(_) => todo!(), // TODO: print error + }; + dropped_backtrack.extend(dropped_seq); + } + + self.verification_key_table + .decrement_hashed(&dropped_backtrack); + + let locally_generated_dropped = dropped_backtrack + .iter() + .filter(|t| self.locally_generated_uncommitted.contains_key(t)) + .collect::>(); + + let dropped_commands = { + let accounts_to_check = accounts; + let existing_account_states_by_id = accounts; + + let get_account = |id: &AccountId| { + match existing_account_states_by_id.get(id) { + Some(account) => account.clone(), + None => { + if accounts_to_check.contains_key(id) { + Account::empty() + } else { + // OCaml panic too, with same message + panic!( + "did not expect Indexed_pool.revalidate to call \ + get_account on account not in accounts_to_check" + ) + } + } + } + }; + + self.pool + .revalidate(RevalidateKind::Subset(accounts_to_check), get_account) + }; + + let (committed_commands, dropped_commit_conflicts): (Vec<_>, Vec<_>) = { + let command_hashes: HashSet = new_commands + .iter() + .map(|cmd| { + let cmd = transaction_hash::hash_command(cmd.data.clone()); + cmd.hash + }) + .collect(); + + dropped_commands + .iter() + .partition(|cmd| command_hashes.contains(&cmd.hash)) + }; + + for cmd in &committed_commands { + self.verification_key_table.decrement_hashed([&**cmd]); + if let Some(data) = self.locally_generated_uncommitted.remove(cmd) { + let old = self + .locally_generated_committed + .insert((*cmd).clone(), data); + assert!(old.is_none()); + }; + } + + let _commit_conflicts_locally_generated = dropped_commit_conflicts + .iter() + .filter(|cmd| self.locally_generated_uncommitted.remove(cmd).is_some()); + + for cmd in locally_generated_dropped { + // If the dropped transaction was included in the winning chain, it'll + // be in locally_generated_committed. If it wasn't, try re-adding to + // the pool. + let remove_cmd = |this: &mut Self| { + this.verification_key_table.decrement_hashed([cmd]); + assert!(this.locally_generated_uncommitted.remove(cmd).is_some()); + }; + + if !self.locally_generated_committed.contains_key(cmd) { + if !self.has_sufficient_fee(pool_max_size, &cmd.data) { + remove_cmd(self) + } else { + let unchecked = &cmd.data; + match accounts.get(&unchecked.fee_payer()) { + Some(account) => { + match self.pool.add_from_gossip_exn( + cmd, + account.nonce, + account.liquid_balance_at_slot(global_slot), + ) { + Err(_) => { + remove_cmd(self); + } + Ok(_) => { + self.verification_key_table.increment_hashed([cmd]); + } + } + } + None => { + remove_cmd(self); + } + } + } + } + } + + let expired_commands = self.pool.remove_expired(); + for cmd in &expired_commands { + self.verification_key_table.decrement_hashed([cmd]); + self.locally_generated_uncommitted.remove(cmd); + } + } + + pub fn get_accounts_to_apply_diff(&self, diff: &diff::DiffVerified) -> BTreeSet { + let fee_payer = |cmd: &ValidCommandWithHash| cmd.data.fee_payer(); + diff.list.iter().map(fee_payer).collect() + } + + fn apply( + &mut self, + diff: &diff::DiffVerified, + accounts: &BTreeMap, + is_sender_local: bool, + ) -> Result< + ( + ApplyDecision, + Vec, + Vec<(ValidCommandWithHash, diff::Error)>, + ), + String, + > { + let fee_payer = |cmd: &ValidCommandWithHash| cmd.data.fee_payer(); + let fee_payer_accounts = accounts; + + let check_command = |pool: &IndexedPool, cmd: &ValidCommandWithHash| { + if pool.member(cmd) { + Err(diff::Error::Duplicate) + } else { + match fee_payer_accounts.get(&fee_payer(cmd)) { + None => Err(diff::Error::FeePayerAccountNotFound), + Some(account) => { + if account.has_permission_to_send() + && account.has_permission_to_increment_nonce() + { + Ok(()) + } else { + Err(diff::Error::FeePayerNotPermittedToSend) + } + } + } + } + }; + + let add_results = diff + .list + .iter() + .map(|cmd| { + let result: Result<_, diff::Error> = (|| { + check_command(&self.pool, cmd)?; + + let global_slot = self.pool.global_slot_since_genesis(); + let account = fee_payer_accounts.get(&fee_payer(cmd)).unwrap(); // OCaml panics too + + match self.pool.add_from_gossip_exn( + cmd, + account.nonce, + account.liquid_balance_at_slot(global_slot), + ) { + Ok(x) => Ok(x), + Err(e) => { + eprintln!(); + Err(e.into()) + } + } + })(); + + match result { + Ok((cmd, dropped)) => Ok((cmd, dropped)), + Err(err) => Err((cmd, err)), + } + }) + .collect::>(); + + let added_cmds = add_results + .iter() + .filter_map(|cmd| match cmd { + Ok((cmd, _)) => Some(cmd), + Err(_) => None, + }) + .collect::>(); + + let dropped_for_add = add_results + .iter() + .filter_map(|cmd| match cmd { + Ok((_, dropped)) => Some(dropped), + Err(_) => None, + }) + .flatten() + .collect::>(); + + let dropped_for_size = { self.drop_until_below_max_size(self.config.pool_max_size) }; + + let all_dropped_cmds = dropped_for_add + .iter() + .map(|cmd| *cmd) + .chain(dropped_for_size.iter()) + .collect::>(); + + let _ = { + self.verification_key_table.increment_hashed(added_cmds); + self.verification_key_table + .decrement_hashed(all_dropped_cmds.iter().map(|cmd| *cmd)); + }; + + let dropped_for_add_hashes: HashSet<&BlakeHash> = + dropped_for_add.iter().map(|cmd| &cmd.hash).collect(); + let dropped_for_size_hashes: HashSet<&BlakeHash> = + dropped_for_size.iter().map(|cmd| &cmd.hash).collect(); + let all_dropped_cmd_hashes: HashSet<&BlakeHash> = dropped_for_add_hashes + .union(&dropped_for_size_hashes) + .map(|hash| *hash) + .collect(); + + // let locally_generated_dropped = all_dropped_cmds + // .iter() + // .filter(|cmd| self.locally_generated_uncommitted.remove(cmd).is_some()); + + if is_sender_local { + for result in add_results.iter() { + let Ok((cmd, _dropped)) = result else { + continue; + }; + if !all_dropped_cmd_hashes.contains(&cmd.hash) { + self.register_locally_generated(cmd); + } + } + } + + let mut accepted = Vec::with_capacity(128); + let mut rejected = Vec::with_capacity(128); + + // TODO: Re-work this to avoid cloning ? + for result in &add_results { + match result { + Ok((cmd, _dropped)) => { + if all_dropped_cmd_hashes.contains(&cmd.hash) { + // ignored (dropped) + } else { + accepted.push(cmd.clone()); + } + } + Err((cmd, error)) => { + rejected.push(((*cmd).clone(), error.clone())); + } + } + } + + let decision = if rejected + .iter() + .any(|(_, error)| error.grounds_for_diff_rejection()) + { + ApplyDecision::Reject + } else { + ApplyDecision::Accept + }; + + Ok((decision, accepted, rejected)) + } + + pub fn unsafe_apply( + &mut self, + diff: &diff::DiffVerified, + accounts: &BTreeMap, + is_sender_local: bool, + ) -> Result< + ( + ApplyDecision, + Vec, + Vec<(UserCommand, diff::Error)>, + ), + String, + > { + let (decision, accepted, rejected) = self.apply(diff, accounts, is_sender_local)?; + let accepted = accepted + .into_iter() + .map(|cmd| cmd.data.forget_check()) + .collect::>(); + let rejected = rejected + .into_iter() + .map(|(cmd, e)| (cmd.data.forget_check(), e)) + .collect::>(); + Ok((decision, accepted, rejected)) + } + + fn register_locally_generated(&mut self, cmd: &ValidCommandWithHash) { + match self.locally_generated_uncommitted.entry(cmd.clone()) { + Entry::Occupied(mut entry) => { + let (time, _batch_num) = entry.get_mut(); + *time = Time::now(); + } + Entry::Vacant(entry) => { + let batch_num = if self.remaining_in_batch > 0 { + self.remaining_in_batch = self.remaining_in_batch - 1; + self.current_batch + } else { + self.remaining_in_batch = MAX_PER_15_SECONDS - 1; + self.current_batch = self.current_batch + 1; + self.current_batch + }; + entry.insert((Time::now(), Batch::Of(batch_num))); + } + } + } + + pub fn verify( + &self, + diff: Envelope, + accounts: BTreeMap, + ) -> Result, String> { + let well_formedness_errors: HashSet<_> = diff + .data() + .list + .iter() + .flat_map(|cmd| match cmd.check_well_formedness() { + Ok(()) => Vec::new(), + Err(errors) => errors, + }) + .collect(); + + if !well_formedness_errors.is_empty() { + return Err(format!( + "Some commands have one or more well-formedness errors: {:?}", + well_formedness_errors + )); + } + + let cs = diff + .data() + .list + .iter() + .cloned() + .map(|cmd| MaybeWithStatus { cmd, status: None }) + .collect::>(); + + let diff = UserCommand::to_all_verifiable::(cs, |account_ids| { + let mempool_vks: HashMap<_, _> = account_ids + .iter() + .map(|id| { + let vks = self.verification_key_table.find_vks_by_account_id(id); + let vks: HashMap<_, _> = + vks.iter().map(|vk| (vk.hash, (*vk).clone())).collect(); + (id.clone(), vks) + }) + .collect(); + + let ledger_vks = UserCommand::load_vks_from_ledger_accounts(&accounts); + let ledger_vks: HashMap<_, _> = ledger_vks + .into_iter() + .map(|(id, vk)| { + let mut map = HashMap::new(); + map.insert(vk.hash, vk); + (id, map) + }) + .collect(); + + let new_map: HashMap> = HashMap::new(); + let merged = + mempool_vks + .into_iter() + .chain(ledger_vks) + .fold(new_map, |mut accum, (id, map)| { + let entry = accum.entry(id).or_default(); + for (hash, vk) in map { + entry.insert(hash, vk); + } + accum + }); + + from_unapplied_sequence::Cache::new(merged) + }) + .map_err(|e| format!("Invalid {:?}", e))?; + + let diff = diff + .into_iter() + .map(|MaybeWithStatus { cmd, status: _ }| WithStatus { + data: cmd, + status: Applied, + }) + .collect::>(); + + Verifier + .verify_commands(diff, None) + .into_iter() + .map(|cmd| { + // TODO: Handle invalids + match cmd { + crate::verifier::VerifyCommandsResult::Valid(cmd) => Ok(cmd), + e => Err(format!("invalid tx: {:?}", e)), + } + }) + .collect() + } + + fn get_rebroadcastable(&mut self, has_timed_out: F) -> Vec> + where + F: Fn(&Time) -> bool, + { + let log = |has_timed_out: bool, s: &str, cmd: &ValidCommandWithHash| -> bool { + if has_timed_out { + eprintln!("{}: {:?}", s, cmd); + false + } else { + true + } + }; + + self.locally_generated_uncommitted + .retain(|key, (time, _batch)| { + log( + has_timed_out(time), + "No longer rebroadcasting uncommitted expired command", + key, + ) + }); + self.locally_generated_committed + .retain(|key, (time, _batch)| { + log( + has_timed_out(time), + "Removing committed locally generated expired command", + key, + ) + }); + + let mut rebroadcastable_txs = self + .locally_generated_uncommitted + .iter() + .collect::>(); + + rebroadcastable_txs.sort_by(|(txn1, (_, batch1)), (txn2, (_, batch2))| { + use std::cmp::Ordering::Equal; + + let get_nonce = + |txn: &ValidCommandWithHash| txn.data.forget_check().applicable_at_nonce(); + + match batch1.cmp(batch2) { + Equal => (), + x => return x, + } + match get_nonce(txn1).cmp(&get_nonce(txn2)) { + Equal => (), + x => return x, + } + txn1.hash.cmp(&txn2.hash) + }); + + rebroadcastable_txs + .into_iter() + .group_by(|(_txn, (_time, batch))| batch) + .into_iter() + .map(|(_batch, group_txns)| { + group_txns + .map(|(txn, _)| txn.data.forget_check()) + .collect::>() + }) + .collect::>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Make sure that the merge in `TransactionPool::verify` is correct + #[test] + fn test_map_merge() { + let mut a = HashMap::new(); + a.insert(1, { + let mut map = HashMap::new(); + map.insert(1, 10); + map.insert(2, 12); + map + }); + let mut b = HashMap::new(); + b.insert(1, { + let mut map = HashMap::new(); + map.insert(3, 20); + map + }); + + let new_map: HashMap<_, HashMap<_, _>> = HashMap::new(); + let merged = a + .into_iter() + .chain(b) + .fold(new_map, |mut accum, (id, map)| { + let entry = accum.entry(id).or_default(); + for (hash, vk) in map { + entry.insert(hash, vk); + } + accum + }); + + let one = merged.get(&1).unwrap(); + assert!(one.get(&1).is_some()); + assert!(one.get(&2).is_some()); + assert!(one.get(&3).is_some()); + + dbg!(merged); + } +} diff --git a/ledger/src/verifier/mod.rs b/ledger/src/verifier/mod.rs index cc4092727..f63ee390b 100644 --- a/ledger/src/verifier/mod.rs +++ b/ledger/src/verifier/mod.rs @@ -103,6 +103,7 @@ fn verify_digest_only(ts: Vec<(LedgerProof, SokMessage)>) -> Result<(), String> } /// https://github.com/MinaProtocol/mina/blob/bfd1009abdbee78979ff0343cc73a3480e862f58/src/lib/verifier/verifier_intf.ml#L10C1-L36C29 +#[derive(Debug)] pub enum VerifyCommandsResult { Valid(valid::UserCommand), ValidAssuming( diff --git a/mina-p2p-messages/src/v2/generated.rs b/mina-p2p-messages/src/v2/generated.rs index d56bee81d..52f0f74a0 100644 --- a/mina-p2p-messages/src/v2/generated.rs +++ b/mina-p2p-messages/src/v2/generated.rs @@ -312,6 +312,21 @@ pub struct SnarkWorkerWorkerRpcsVersionedSubmitWorkV2TQuery { pub prover: NonZeroCurvePoint, } +/// **OCaml name**: `Mina_base__User_command.Verifiable.Stable.V2` +/// +/// Gid: `841` +/// Location: [src/lib/mina_base/user_command.ml:144:6](https://github.com/MinaProtocol/mina/blob/src/lib/mina_base/user_command.ml#L144) +/// +/// +/// Gid: `837` +/// Location: [src/lib/mina_base/user_command.ml:7:6](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/mina_base/user_command.ml#L7) +/// Args: MinaBaseSignedCommandStableV2 , MinaBaseZkappCommandVerifiableStableV1 +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite)] +pub enum MinaBaseUserCommandVerifiableStableV2 { + SignedCommand(MinaBaseSignedCommandStableV2), + ZkappCommand(MinaBaseZkappCommandVerifiableStableV1), +} + /// Derived name: `Pickles__Proof.Proofs_verified_2.Repr.Stable.V2.statement.fp` /// /// Gid: `461` @@ -978,6 +993,17 @@ pub struct CurrencyAmountStableV1(pub UnsignedExtendedUInt64Int64ForVersionTagsS #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite, Deref)] pub struct CurrencyBalanceStableV1(pub CurrencyAmountStableV1); +/// Derived name: `Mina_base__Zkapp_command.Verifiable.Stable.V1.account_updates.data.a` +/// +/// Gid: `645` +/// Location: [src/lib/with_hash/with_hash.ml:8:4](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/with_hash/with_hash.ml#L8) +/// Args: MinaBaseVerificationKeyWireStableV1 , crate :: bigint :: BigInt +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite)] +pub struct MinaBaseZkappCommandVerifiableStableV1AccountUpdatesDataA { + pub data: MinaBaseVerificationKeyWireStableV1, + pub hash: crate::bigint::BigInt, +} + /// **OCaml name**: `Data_hash_lib__State_hash.Stable.V1` /// /// Gid: `651` @@ -1891,6 +1917,17 @@ pub struct MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesAACallsA { pub stack_hash: (), } +/// Derived name: `Mina_base__Zkapp_command.Verifiable.Stable.V1.account_updates.a.a.calls.a` +/// +/// Gid: `818` +/// Location: [src/lib/mina_base/with_stack_hash.ml:6:4](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/mina_base/with_stack_hash.ml#L6) +/// Args: Box < MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA > , MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1 +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite)] +pub struct MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAACallsA { + pub elt: Box, + pub stack_hash: MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1, +} + /// Derived name: `Mina_base__Zkapp_command.T.Stable.V1.Wire.Stable.V1.account_updates.a` /// /// Gid: `818` @@ -1902,6 +1939,17 @@ pub struct MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA { pub stack_hash: (), } +/// Derived name: `Mina_base__Zkapp_command.Verifiable.Stable.V1.account_updates.a` +/// +/// Gid: `818` +/// Location: [src/lib/mina_base/with_stack_hash.ml:6:4](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/mina_base/with_stack_hash.ml#L6) +/// Args: MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA , MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1 +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite)] +pub struct MinaBaseZkappCommandVerifiableStableV1AccountUpdatesA { + pub elt: MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA, + pub stack_hash: MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1, +} + /// Derived name: `Mina_transaction_logic.Transaction_applied.Coinbase_applied.Stable.V2.coinbase` /// /// Gid: `819` @@ -1957,6 +2005,21 @@ pub struct MinaTransactionLogicTransactionAppliedZkappCommandAppliedStableV1Comm pub status: MinaBaseTransactionStatusStableV2, } +/// Derived name: `Mina_base__Zkapp_command.Verifiable.Stable.V1.account_updates.a.a` +/// +/// Gid: `820` +/// Location: [src/lib/mina_base/zkapp_command.ml:11:8](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/mina_base/zkapp_command.ml#L11) +/// Args: (MinaBaseAccountUpdateTStableV1 , Option < MinaBaseZkappCommandVerifiableStableV1AccountUpdatesDataA > ,) , MinaBaseZkappCommandCallForestMakeDigestStrAccountUpdateStableV1 , MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1 +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite)] +pub struct MinaBaseZkappCommandVerifiableStableV1AccountUpdatesAA { + pub account_update: ( + MinaBaseAccountUpdateTStableV1, + Option, + ), + pub account_update_digest: MinaBaseZkappCommandCallForestMakeDigestStrAccountUpdateStableV1, + pub calls: List, +} + /// Derived name: `Mina_base__Zkapp_command.T.Stable.V1.Wire.Stable.V1.account_updates.a.a` /// /// Gid: `820` @@ -1969,6 +2032,22 @@ pub struct MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesAA { pub calls: List, } +/// **OCaml name**: `Mina_base__Zkapp_command.Call_forest.Make_digest_str.Account_update.Stable.V1` +/// +/// Gid: `821` +/// Location: [src/lib/mina_base/zkapp_command.ml:224:10](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/mina_base/zkapp_command.ml#L224) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite, Deref)] +pub struct MinaBaseZkappCommandCallForestMakeDigestStrAccountUpdateStableV1( + pub crate::bigint::BigInt, +); + +/// **OCaml name**: `Mina_base__Zkapp_command.Call_forest.Make_digest_str.Forest.Stable.V1` +/// +/// Gid: `822` +/// Location: [src/lib/mina_base/zkapp_command.ml:253:10](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/mina_base/zkapp_command.ml#L253) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite, Deref)] +pub struct MinaBaseZkappCommandCallForestMakeDigestStrForestStableV1(pub crate::bigint::BigInt); + /// **OCaml name**: `Mina_base__Zkapp_command.T.Stable.V1.Wire.Stable.V1` /// /// Gid: `829` @@ -1980,6 +2059,17 @@ pub struct MinaBaseZkappCommandTStableV1WireStableV1 { pub memo: MinaBaseSignedCommandMemoStableV1, } +/// **OCaml name**: `Mina_base__Zkapp_command.Verifiable.Stable.V1` +/// +/// Gid: `832` +/// Location: [src/lib/mina_base/zkapp_command.ml:1096:6](https://github.com/MinaProtocol/mina/blob/1551e2faaa/src/lib/mina_base/zkapp_command.ml#L1096) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, BinProtRead, BinProtWrite)] +pub struct MinaBaseZkappCommandVerifiableStableV1 { + pub fee_payer: MinaBaseAccountUpdateFeePayerStableV1, + pub account_updates: List, + pub memo: MinaBaseSignedCommandMemoStableV1, +} + /// **OCaml name**: `Mina_base__User_command.Stable.V2` /// /// Gid: `839` diff --git a/mina-p2p-messages/types-v2.txt b/mina-p2p-messages/types-v2.txt index 390fbc662..53432fcbb 100644 --- a/mina-p2p-messages/types-v2.txt +++ b/mina-p2p-messages/types-v2.txt @@ -18,3 +18,4 @@ Transaction_witness.Stable.V2.t Prover.Extend_blockchain_input.Stable.V2.t Snark_worker.Worker.Rpcs_versioned.Get_work.V2.T.response Snark_worker.Worker.Rpcs_versioned.Submit_work.V2.T.query +Mina_base__User_command.Verifiable.Stable.V2.t diff --git a/node/native/src/service.rs b/node/native/src/service.rs index 9c85f4213..1dde2232b 100644 --- a/node/native/src/service.rs +++ b/node/native/src/service.rs @@ -3,11 +3,13 @@ use std::collections::{BTreeMap, VecDeque}; use std::sync::{Arc, Mutex}; use ledger::scan_state::scan_state::transaction_snark::{SokDigest, Statement}; +use ledger::scan_state::transaction_logic::{verifiable, WithStatus}; +use ledger::verifier::Verifier; use libp2p_identity::Keypair; use mina_p2p_messages::v2::{LedgerProofProdStableV2, TransactionSnarkWorkTStableV2Proofs}; -#[cfg(feature = "p2p-libp2p")] use node::p2p::service_impl::mio::MioService; use node::p2p::service_impl::services::NativeP2pNetworkService; +use node::snark::user_command_verify::{SnarkUserCommandVerifyId, SnarkUserCommandVerifyService}; use rand::prelude::*; use redux::ActionMeta; use serde::Serialize; @@ -227,6 +229,35 @@ impl SnarkBlockVerifyService for NodeService { } } +impl SnarkUserCommandVerifyService for NodeService { + fn verify_init( + &mut self, + req_id: SnarkUserCommandVerifyId, + commands: Vec>, + _verifier_index: Arc, + _verifier_srs: Arc>, + ) { + if self.replayer.is_some() { + return; + } + let tx = self.event_sender.clone(); + rayon::spawn_fifo(move || { + let verifieds: Vec<_> = Verifier + .verify_commands(commands, None) + .into_iter() + .map(|cmd| { + // TODO: Handle invalids + match cmd { + ledger::verifier::VerifyCommandsResult::Valid(cmd) => Ok(cmd), + e => Err(format!("invalid tx: {:?}", e)), + } + }) + .collect(); + let _ = tx.send(SnarkEvent::UserCommandVerify(req_id, verifieds).into()); + }); + } +} + impl SnarkWorkVerifyService for NodeService { fn verify_init( &mut self, diff --git a/node/src/action.rs b/node/src/action.rs index 980ed5f25..3954846bb 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -12,6 +12,7 @@ pub use crate::p2p::P2pAction; pub use crate::rpc::RpcAction; pub use crate::snark::SnarkAction; pub use crate::snark_pool::SnarkPoolAction; +pub use crate::transaction_pool::TransactionPoolAction; pub use crate::transition_frontier::TransitionFrontierAction; pub use crate::watched_accounts::WatchedAccountsAction; @@ -30,6 +31,7 @@ pub enum Action { Consensus(ConsensusAction), TransitionFrontier(TransitionFrontierAction), SnarkPool(SnarkPoolAction), + TransactionPool(TransactionPoolAction), ExternalSnarkWorker(ExternalSnarkWorkerAction), BlockProducer(BlockProducerAction), Rpc(RpcAction), diff --git a/node/src/action_kind.rs b/node/src/action_kind.rs index 54ff674e4..a1c0fb3d0 100644 --- a/node/src/action_kind.rs +++ b/node/src/action_kind.rs @@ -50,10 +50,12 @@ use crate::p2p::peer::P2pPeerAction; use crate::p2p::P2pAction; use crate::rpc::RpcAction; use crate::snark::block_verify::SnarkBlockVerifyAction; +use crate::snark::user_command_verify::SnarkUserCommandVerifyAction; use crate::snark::work_verify::SnarkWorkVerifyAction; use crate::snark::SnarkAction; use crate::snark_pool::candidate::SnarkPoolCandidateAction; use crate::snark_pool::SnarkPoolAction; +use crate::transaction_pool::TransactionPoolAction; use crate::transition_frontier::genesis::TransitionFrontierGenesisAction; use crate::transition_frontier::sync::ledger::snarked::TransitionFrontierSyncLedgerSnarkedAction; use crate::transition_frontier::sync::ledger::staged::TransitionFrontierSyncLedgerStagedAction; @@ -339,11 +341,23 @@ pub enum ActionKind { SnarkPoolCandidateWorkVerifyNext, SnarkPoolCandidateWorkVerifyPending, SnarkPoolCandidateWorkVerifySuccess, + SnarkUserCommandVerifyError, + SnarkUserCommandVerifyFinish, + SnarkUserCommandVerifyInit, + SnarkUserCommandVerifyPending, + SnarkUserCommandVerifySuccess, SnarkWorkVerifyError, SnarkWorkVerifyFinish, SnarkWorkVerifyInit, SnarkWorkVerifyPending, SnarkWorkVerifySuccess, + TransactionPoolApplyTransitionFrontierDiff, + TransactionPoolApplyTransitionFrontierDiffWithAccounts, + TransactionPoolApplyVerifiedDiff, + TransactionPoolApplyVerifiedDiffWithAccounts, + TransactionPoolBestTipChanged, + TransactionPoolBestTipChangedWithAccounts, + TransactionPoolRebroadcast, TransitionFrontierGenesisInject, TransitionFrontierSynced, TransitionFrontierGenesisLedgerLoadInit, @@ -430,7 +444,7 @@ pub enum ActionKind { } impl ActionKind { - pub const COUNT: u16 = 358; + pub const COUNT: u16 = 370; } impl std::fmt::Display for ActionKind { @@ -450,6 +464,7 @@ impl ActionKindGet for Action { Self::Consensus(a) => a.kind(), Self::TransitionFrontier(a) => a.kind(), Self::SnarkPool(a) => a.kind(), + Self::TransactionPool(a) => a.kind(), Self::ExternalSnarkWorker(a) => a.kind(), Self::BlockProducer(a) => a.kind(), Self::Rpc(a) => a.kind(), @@ -503,6 +518,7 @@ impl ActionKindGet for SnarkAction { match self { Self::BlockVerify(a) => a.kind(), Self::WorkVerify(a) => a.kind(), + Self::UserCommandVerify(a) => a.kind(), } } } @@ -551,6 +567,28 @@ impl ActionKindGet for SnarkPoolAction { } } +impl ActionKindGet for TransactionPoolAction { + fn kind(&self) -> ActionKind { + match self { + Self::BestTipChanged { .. } => ActionKind::TransactionPoolBestTipChanged, + Self::BestTipChangedWithAccounts { .. } => { + ActionKind::TransactionPoolBestTipChangedWithAccounts + } + Self::ApplyVerifiedDiff { .. } => ActionKind::TransactionPoolApplyVerifiedDiff, + Self::ApplyVerifiedDiffWithAccounts { .. } => { + ActionKind::TransactionPoolApplyVerifiedDiffWithAccounts + } + Self::ApplyTransitionFrontierDiff { .. } => { + ActionKind::TransactionPoolApplyTransitionFrontierDiff + } + Self::ApplyTransitionFrontierDiffWithAccounts { .. } => { + ActionKind::TransactionPoolApplyTransitionFrontierDiffWithAccounts + } + Self::Rebroadcast => ActionKind::TransactionPoolRebroadcast, + } + } +} + impl ActionKindGet for ExternalSnarkWorkerAction { fn kind(&self) -> ActionKind { match self { @@ -799,6 +837,18 @@ impl ActionKindGet for SnarkWorkVerifyAction { } } +impl ActionKindGet for SnarkUserCommandVerifyAction { + fn kind(&self) -> ActionKind { + match self { + Self::Init { .. } => ActionKind::SnarkUserCommandVerifyInit, + Self::Pending { .. } => ActionKind::SnarkUserCommandVerifyPending, + Self::Error { .. } => ActionKind::SnarkUserCommandVerifyError, + Self::Success { .. } => ActionKind::SnarkUserCommandVerifySuccess, + Self::Finish { .. } => ActionKind::SnarkUserCommandVerifyFinish, + } + } +} + impl ActionKindGet for TransitionFrontierGenesisAction { fn kind(&self) -> ActionKind { match self { diff --git a/node/src/effects.rs b/node/src/effects.rs index df1105f44..f5cafc246 100644 --- a/node/src/effects.rs +++ b/node/src/effects.rs @@ -12,6 +12,7 @@ use crate::rpc::rpc_effects; use crate::snark::snark_effects; use crate::snark_pool::candidate::SnarkPoolCandidateAction; use crate::snark_pool::{snark_pool_effects, SnarkPoolAction}; +use crate::transaction_pool::transaction_pool_effects; use crate::transition_frontier::genesis::TransitionFrontierGenesisAction; use crate::transition_frontier::transition_frontier_effects; use crate::watched_accounts::watched_accounts_effects; @@ -63,6 +64,9 @@ pub fn effects(store: &mut Store, action: ActionWithMeta) { Action::Consensus(action) => { consensus_effects(store, meta.with_action(action)); } + Action::TransactionPool(action) => { + transaction_pool_effects(store, meta.with_action(action)); + } Action::TransitionFrontier(action) => { transition_frontier_effects(store, meta.with_action(action)); } diff --git a/node/src/event_source/event_source_effects.rs b/node/src/event_source/event_source_effects.rs index d688f9d7c..94354319d 100644 --- a/node/src/event_source/event_source_effects.rs +++ b/node/src/event_source/event_source_effects.rs @@ -1,3 +1,6 @@ +use p2p::channels::snark::P2pChannelsSnarkAction; +use snark::user_command_verify::{SnarkUserCommandVerifyAction, SnarkUserCommandVerifyError}; + use crate::action::CheckTimeoutsAction; use crate::block_producer::vrf_evaluator::BlockProducerVrfEvaluatorAction; use crate::block_producer::{BlockProducerEvent, BlockProducerVrfEvaluatorEvent}; @@ -6,7 +9,6 @@ use crate::ledger::read::LedgerReadAction; use crate::ledger::write::LedgerWriteAction; use crate::p2p::channels::best_tip::P2pChannelsBestTipAction; use crate::p2p::channels::rpc::P2pChannelsRpcAction; -use crate::p2p::channels::snark::P2pChannelsSnarkAction; use crate::p2p::channels::snark_job_commitment::P2pChannelsSnarkJobCommitmentAction; use crate::p2p::channels::{ChannelId, P2pChannelsMessageReceivedAction}; use crate::p2p::connection::incoming::P2pConnectionIncomingAction; @@ -262,6 +264,16 @@ pub fn event_source_effects(store: &mut Store, action: EventSourc store.dispatch(SnarkWorkVerifyAction::Success { req_id }); } }, + SnarkEvent::UserCommandVerify(req_id, result) => { + if result.iter().any(|res| res.is_err()) { + store.dispatch(SnarkUserCommandVerifyAction::Error { + req_id, + error: SnarkUserCommandVerifyError::VerificationFailed, + }); + } else { + store.dispatch(SnarkUserCommandVerifyAction::Success { req_id }); + } + } }, Event::Rpc(rpc_id, e) => match e { RpcRequest::StateGet(filter) => { diff --git a/node/src/lib.rs b/node/src/lib.rs index 8080d53f2..5d7397aac 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -38,6 +38,7 @@ pub mod p2p; pub mod rpc; pub mod snark; pub mod snark_pool; +pub mod transaction_pool; pub mod transition_frontier; pub mod watched_accounts; diff --git a/node/src/reducer.rs b/node/src/reducer.rs index 33b897104..71916af12 100644 --- a/node/src/reducer.rs +++ b/node/src/reducer.rs @@ -27,6 +27,9 @@ pub fn reducer(state: &mut State, action: &ActionWithMeta) { Action::SnarkPool(a) => { state.snark_pool.reducer(meta.with_action(a)); } + Action::TransactionPool(a) => { + state.transaction_pool.reducer(meta.with_action(a)); + } Action::BlockProducer(a) => { state .block_producer diff --git a/node/src/service.rs b/node/src/service.rs index d5479c209..277f4605a 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -17,6 +17,7 @@ pub use crate::transition_frontier::genesis::TransitionFrontierGenesisService; pub use crate::transition_frontier::sync::ledger::snarked::TransitionFrontierSyncLedgerSnarkedService; use p2p::P2pNetworkService; pub use redux::TimeService; +use snark::user_command_verify::SnarkUserCommandVerifyService; use crate::stats::Stats; @@ -25,6 +26,7 @@ pub trait Service: + EventSourceService + SnarkBlockVerifyService + SnarkWorkVerifyService + + SnarkUserCommandVerifyService + P2pConnectionService + P2pDisconnectionService + P2pChannelsService diff --git a/node/src/snark/mod.rs b/node/src/snark/mod.rs index 418841c4d..eba3fa8fc 100644 --- a/node/src/snark/mod.rs +++ b/node/src/snark/mod.rs @@ -1,6 +1,7 @@ pub use ::snark::*; pub mod block_verify; +pub mod user_command_verify; pub mod work_verify; mod snark_effects; diff --git a/node/src/snark/snark_effects.rs b/node/src/snark/snark_effects.rs index 75bd5541f..51e6dab73 100644 --- a/node/src/snark/snark_effects.rs +++ b/node/src/snark/snark_effects.rs @@ -59,5 +59,8 @@ pub fn snark_effects(store: &mut Store, action: SnarkActionWithMe } a.effects(&meta, store); } + SnarkAction::UserCommandVerify(a) => { + a.effects(&meta, store); + } } } diff --git a/node/src/snark/user_command_verify/mod.rs b/node/src/snark/user_command_verify/mod.rs new file mode 100644 index 000000000..efead2cea --- /dev/null +++ b/node/src/snark/user_command_verify/mod.rs @@ -0,0 +1,3 @@ +pub use ::snark::user_command_verify::*; + +mod snark_user_command_verify_actions; diff --git a/node/src/snark/user_command_verify/snark_user_command_verify_actions.rs b/node/src/snark/user_command_verify/snark_user_command_verify_actions.rs new file mode 100644 index 000000000..8b83d1565 --- /dev/null +++ b/node/src/snark/user_command_verify/snark_user_command_verify_actions.rs @@ -0,0 +1,13 @@ +use super::*; + +impl redux::EnablingCondition for SnarkUserCommandVerifyAction { + fn is_enabled(&self, state: &crate::State, time: redux::Timestamp) -> bool { + self.is_enabled(&state.snark, time) + } +} + +impl From for crate::Action { + fn from(value: SnarkUserCommandVerifyAction) -> Self { + Self::Snark(value.into()) + } +} diff --git a/node/src/state.rs b/node/src/state.rs index 73c46457f..ee5910515 100644 --- a/node/src/state.rs +++ b/node/src/state.rs @@ -11,6 +11,7 @@ pub use crate::p2p::P2pState; pub use crate::rpc::RpcState; pub use crate::snark::SnarkState; pub use crate::snark_pool::SnarkPoolState; +use crate::transaction_pool::TransactionPoolState; pub use crate::transition_frontier::TransitionFrontierState; pub use crate::watched_accounts::WatchedAccountsState; use crate::ActionWithMeta; @@ -27,6 +28,7 @@ pub struct State { pub transition_frontier: TransitionFrontierState, pub snark_pool: SnarkPoolState, pub external_snark_worker: ExternalSnarkWorkers, + pub transaction_pool: TransactionPoolState, pub block_producer: BlockProducerState, pub rpc: RpcState, @@ -50,6 +52,7 @@ impl State { external_snark_worker: ExternalSnarkWorkers::new(now), block_producer: BlockProducerState::new(now, config.block_producer), rpc: RpcState::new(), + transaction_pool: TransactionPoolState::new(), watched_accounts: WatchedAccountsState::new(), diff --git a/node/src/transaction_pool/mod.rs b/node/src/transaction_pool/mod.rs new file mode 100644 index 000000000..53aeb6cb0 --- /dev/null +++ b/node/src/transaction_pool/mod.rs @@ -0,0 +1,169 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::{Arc, Mutex}, +}; + +use ledger::{ + scan_state::transaction_logic::{verifiable, UserCommand, WithStatus}, + transaction_pool::{diff, ApplyDecision}, + Account, AccountId, BaseLedger, +}; +use mina_p2p_messages::v2::LedgerHash; +use snark::{user_command_verify::SnarkUserCommandVerifyId, VerifierIndex, VerifierSRS}; + +use crate::{Service, Store}; + +pub mod transaction_pool_actions; + +pub use transaction_pool_actions::TransactionPoolAction; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct TransactionPoolState { + pool: ledger::transaction_pool::TransactionPool, +} + +type TransactionPoolActionWithMeta = redux::ActionWithMeta; +type TransactionPoolActionWithMetaRef<'a> = redux::ActionWithMeta<&'a TransactionPoolAction>; + +impl TransactionPoolState { + pub fn new() -> Self { + Self { + pool: ledger::transaction_pool::TransactionPool::new(), + } + } + + fn rebroadcast(&self, _accepted: Vec, _rejected: Vec<(UserCommand, diff::Error)>) { + // TODO + } + + pub fn reducer(&mut self, action: TransactionPoolActionWithMetaRef<'_>) { + use TransactionPoolAction::*; + + let (action, _meta) = action.split(); + match action { + BestTipChanged { best_tip_hash: _ } => {} + BestTipChangedWithAccounts { accounts } => { + self.pool.on_new_best_tip(accounts); + } + ApplyVerifiedDiff { + best_tip_hash: _, + diff: _, + is_sender_local: _, + } => {} + ApplyVerifiedDiffWithAccounts { + diff, + is_sender_local, + accounts, + } => match self.pool.unsafe_apply(diff, &accounts, *is_sender_local) { + Ok((ApplyDecision::Accept, accepted, rejected)) => { + self.rebroadcast(accepted, rejected) + } + Ok((ApplyDecision::Reject, accepted, rejected)) => { + self.rebroadcast(accepted, rejected) + } + Err(e) => eprintln!("unsafe_apply error: {:?}", e), + }, + ApplyTransitionFrontierDiff { + best_tip_hash: _, + diff: _, + } => {} + ApplyTransitionFrontierDiffWithAccounts { diff, accounts } => { + self.pool.handle_transition_frontier_diff(diff, &accounts); + } + Rebroadcast => {} + } + } +} + +fn load_accounts_from_ledger( + store: &mut Store, + best_tip_hash: &LedgerHash, + account_ids: BTreeSet, +) -> BTreeMap { + let (best_tip_mask, _) = store + .service + .ledger_manager() + .get_mask(&best_tip_hash) + .unwrap(); // TODO Handle error + + account_ids + .into_iter() + .filter_map(|account_id| { + best_tip_mask + .location_of_account(&account_id) + .and_then(|addr| { + best_tip_mask + .get(addr) + .map(|account| (account_id, *account)) + }) + }) + .collect::>() +} + +pub fn transaction_pool_effects( + store: &mut Store, + action: TransactionPoolActionWithMeta, +) { + let (action, _meta) = action.split(); + + match action { + TransactionPoolAction::BestTipChanged { best_tip_hash } => { + let state = &store.state().transaction_pool; + let account_ids = state.pool.get_accounts_to_revalidate_on_new_best_tip(); + + let accounts = load_accounts_from_ledger(store, &best_tip_hash, account_ids); + + store.dispatch(TransactionPoolAction::BestTipChangedWithAccounts { accounts }); + } + TransactionPoolAction::BestTipChangedWithAccounts { accounts: _ } => {} + TransactionPoolAction::ApplyVerifiedDiff { + best_tip_hash, + diff, + is_sender_local, + } => { + let state = &store.state().transaction_pool; + let account_ids = state.pool.get_accounts_to_apply_diff(&diff); + + let accounts = load_accounts_from_ledger(store, &best_tip_hash, account_ids); + + store.dispatch(TransactionPoolAction::ApplyVerifiedDiffWithAccounts { + diff, + is_sender_local, + accounts, + }); + } + TransactionPoolAction::ApplyVerifiedDiffWithAccounts { + diff: _, + is_sender_local: _, + accounts: _, + } => {} + TransactionPoolAction::ApplyTransitionFrontierDiff { + best_tip_hash, + diff, + } => { + let state = &store.state().transaction_pool; + let account_ids = state.pool.get_accounts_to_handle_transition_diff(&diff); + + let accounts = load_accounts_from_ledger(store, &best_tip_hash, account_ids); + + store.dispatch( + TransactionPoolAction::ApplyTransitionFrontierDiffWithAccounts { diff, accounts }, + ); + } + TransactionPoolAction::ApplyTransitionFrontierDiffWithAccounts { + diff: _, + accounts: _, + } => {} + TransactionPoolAction::Rebroadcast => {}, + } +} + +pub trait VerifyUserCommandsService: redux::Service { + fn verify_init( + &mut self, + req_id: SnarkUserCommandVerifyId, + commands: Vec>, + verifier_index: Arc, + verifier_srs: Arc>, + ); +} diff --git a/node/src/transaction_pool/transaction_pool_actions.rs b/node/src/transaction_pool/transaction_pool_actions.rs new file mode 100644 index 000000000..264423862 --- /dev/null +++ b/node/src/transaction_pool/transaction_pool_actions.rs @@ -0,0 +1,46 @@ +use std::collections::BTreeMap; + +use ledger::{ + transaction_pool::diff::{BestTipDiff, DiffVerified}, + Account, AccountId, +}; +use mina_p2p_messages::v2::LedgerHash; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum TransactionPoolAction { + BestTipChanged { + best_tip_hash: LedgerHash, + }, + BestTipChangedWithAccounts { + accounts: BTreeMap, + }, + ApplyVerifiedDiff { + best_tip_hash: LedgerHash, + diff: DiffVerified, + /// Diff was crearted locally, or from remote peer ? + is_sender_local: bool, + }, + ApplyVerifiedDiffWithAccounts { + diff: DiffVerified, + is_sender_local: bool, + accounts: BTreeMap, + }, + ApplyTransitionFrontierDiff { + best_tip_hash: LedgerHash, + diff: BestTipDiff, + }, + ApplyTransitionFrontierDiffWithAccounts { + diff: BestTipDiff, + accounts: BTreeMap, + }, + /// Rebroadcast locally generated pool items every 10 minutes. Do so for 50 + /// minutes - at most 5 rebroadcasts - before giving up. + Rebroadcast, +} + +impl redux::EnablingCondition for TransactionPoolAction { + fn is_enabled(&self, _state: &crate::State, _time: redux::Timestamp) -> bool { + true + } +} diff --git a/node/testing/src/service/mod.rs b/node/testing/src/service/mod.rs index 66feb4901..a78511b05 100644 --- a/node/testing/src/service/mod.rs +++ b/node/testing/src/service/mod.rs @@ -8,6 +8,7 @@ use std::{collections::BTreeMap, ffi::OsStr, sync::Arc}; use ledger::dummy::dummy_transaction_proof; use ledger::proofs::transaction::ProofError; use ledger::scan_state::scan_state::transaction_snark::SokMessage; +use ledger::scan_state::transaction_logic::{verifiable, WithStatus}; use ledger::Mask; use mina_p2p_messages::string::ByteString; use mina_p2p_messages::v2::{ @@ -31,6 +32,7 @@ use node::service::{ use node::snark::block_verify::{ SnarkBlockVerifyId, SnarkBlockVerifyService, VerifiableBlockWithHash, }; +use node::snark::user_command_verify::{SnarkUserCommandVerifyId, SnarkUserCommandVerifyService}; use node::snark::work_verify::{SnarkWorkVerifyId, SnarkWorkVerifyService}; use node::snark::{SnarkEvent, VerifierIndex, VerifierSRS}; use node::snark_pool::{JobState, SnarkPoolService}; @@ -391,6 +393,24 @@ impl SnarkBlockVerifyService for NodeTestingService { } } +impl SnarkUserCommandVerifyService for NodeTestingService { + fn verify_init( + &mut self, + req_id: SnarkUserCommandVerifyId, + commands: Vec>, + verifier_index: Arc, + verifier_srs: Arc>, + ) { + SnarkUserCommandVerifyService::verify_init( + &mut self.real, + req_id, + commands, + verifier_index, + verifier_srs, + ) + } +} + impl SnarkWorkVerifyService for NodeTestingService { fn verify_init( &mut self, diff --git a/snark/src/lib.rs b/snark/src/lib.rs index e233c8c5c..ed1b1c004 100644 --- a/snark/src/lib.rs +++ b/snark/src/lib.rs @@ -10,6 +10,7 @@ pub use ledger::proofs::verifier_index::{get_verifier_index, VerifierKind}; pub use merkle_path::calc_merkle_root_hash; pub mod block_verify; +pub mod user_command_verify; pub mod work_verify; mod snark_event; diff --git a/snark/src/snark_actions.rs b/snark/src/snark_actions.rs index 1407945ca..91abd28b7 100644 --- a/snark/src/snark_actions.rs +++ b/snark/src/snark_actions.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::user_command_verify::SnarkUserCommandVerifyAction; + use super::block_verify::SnarkBlockVerifyAction; use super::work_verify::SnarkWorkVerifyAction; @@ -10,4 +12,5 @@ pub type SnarkActionWithMetaRef<'a> = redux::ActionWithMeta<&'a SnarkAction>; pub enum SnarkAction { BlockVerify(SnarkBlockVerifyAction), WorkVerify(SnarkWorkVerifyAction), + UserCommandVerify(SnarkUserCommandVerifyAction), } diff --git a/snark/src/snark_event.rs b/snark/src/snark_event.rs index a863d471d..3e0e0c9fa 100644 --- a/snark/src/snark_event.rs +++ b/snark/src/snark_event.rs @@ -1,12 +1,18 @@ +use ledger::scan_state::transaction_logic::valid; use serde::{Deserialize, Serialize}; use super::block_verify::{SnarkBlockVerifyError, SnarkBlockVerifyId}; use super::work_verify::{SnarkWorkVerifyError, SnarkWorkVerifyId}; +use crate::user_command_verify::SnarkUserCommandVerifyId; #[derive(Serialize, Deserialize, Debug, Clone)] pub enum SnarkEvent { BlockVerify(SnarkBlockVerifyId, Result<(), SnarkBlockVerifyError>), WorkVerify(SnarkWorkVerifyId, Result<(), SnarkWorkVerifyError>), + UserCommandVerify( + SnarkUserCommandVerifyId, + Vec>, + ), } fn res_kind(res: &Result) -> &'static str { @@ -26,6 +32,15 @@ impl std::fmt::Display for SnarkEvent { Self::WorkVerify(id, res) => { write!(f, "WorkVerify, {id}, {}", res_kind(res)) } + Self::UserCommandVerify(id, res) => { + let n_failed = res.iter().filter(|res| res.is_err()).count(); + let n_success = res.len() - n_failed; + write!( + f, + "UserCommandVerify, {id}, n_success={} n_failed={}", + n_success, n_failed + ) + } } } } diff --git a/snark/src/snark_reducer.rs b/snark/src/snark_reducer.rs index ca7547f1b..f321fdd8b 100644 --- a/snark/src/snark_reducer.rs +++ b/snark/src/snark_reducer.rs @@ -6,6 +6,9 @@ impl SnarkState { match action { SnarkAction::BlockVerify(a) => self.block_verify.reducer(meta.with_action(a)), SnarkAction::WorkVerify(a) => self.work_verify.reducer(meta.with_action(a)), + SnarkAction::UserCommandVerify(a) => { + self.user_command_verify.reducer(meta.with_action(a)) + } } } } diff --git a/snark/src/snark_state.rs b/snark/src/snark_state.rs index 507b19872..24e110475 100644 --- a/snark/src/snark_state.rs +++ b/snark/src/snark_state.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +use crate::user_command_verify::SnarkUserCommandVerifyState; use crate::SnarkConfig; use super::block_verify::SnarkBlockVerifyState; @@ -9,6 +10,7 @@ use super::work_verify::SnarkWorkVerifyState; pub struct SnarkState { pub block_verify: SnarkBlockVerifyState, pub work_verify: SnarkWorkVerifyState, + pub user_command_verify: SnarkUserCommandVerifyState, } impl SnarkState { @@ -19,6 +21,10 @@ impl SnarkState { config.block_verifier_srs, ), work_verify: SnarkWorkVerifyState::new( + config.work_verifier_index.clone(), + config.work_verifier_srs.clone(), + ), + user_command_verify: SnarkUserCommandVerifyState::new( config.work_verifier_index, config.work_verifier_srs, ), diff --git a/snark/src/user_command_verify/mod.rs b/snark/src/user_command_verify/mod.rs new file mode 100644 index 000000000..685ed5107 --- /dev/null +++ b/snark/src/user_command_verify/mod.rs @@ -0,0 +1,32 @@ +mod snark_user_command_verify_state; +pub use snark_user_command_verify_state::*; + +mod snark_user_command_verify_actions; +pub use snark_user_command_verify_actions::*; + +mod snark_user_command_verify_reducer; + +mod snark_user_command_verify_effects; + +mod snark_user_command_verify_service; +pub use snark_user_command_verify_service::*; + +use serde::{Deserialize, Serialize}; + +pub struct SnarkUserCommandVerifyIdType; +impl openmina_core::requests::RequestIdType for SnarkUserCommandVerifyIdType { + fn request_id_type() -> &'static str { + "SnarkUserCommandVerifyId" + } +} + +pub type SnarkUserCommandVerifyId = + openmina_core::requests::RequestId; + +#[derive(Serialize, Deserialize, Debug, Clone, thiserror::Error)] +pub enum SnarkUserCommandVerifyError { + #[error("verification failed")] + VerificationFailed, + #[error("validator thread crashed")] + ValidatorThreadCrashed, +} diff --git a/snark/src/user_command_verify/snark_user_command_verify_actions.rs b/snark/src/user_command_verify/snark_user_command_verify_actions.rs new file mode 100644 index 000000000..d30d85e30 --- /dev/null +++ b/snark/src/user_command_verify/snark_user_command_verify_actions.rs @@ -0,0 +1,65 @@ +use ledger::scan_state::transaction_logic::{verifiable, WithStatus}; +use serde::{Deserialize, Serialize}; + +use openmina_core::ActionEvent; + +use super::{SnarkUserCommandVerifyError, SnarkUserCommandVerifyId}; + +pub type SnarkUserCommandVerifyActionWithMeta = redux::ActionWithMeta; +pub type SnarkUserCommandVerifyActionWithMetaRef<'a> = + redux::ActionWithMeta<&'a SnarkUserCommandVerifyAction>; + +#[derive(Serialize, Deserialize, Debug, Clone, ActionEvent)] +#[action_event(level = trace, fields(display(req_id), display(error)))] +pub enum SnarkUserCommandVerifyAction { + #[action_event(level = info)] + Init { + req_id: SnarkUserCommandVerifyId, + commands: Vec>, + sender: String, + }, + Pending { + req_id: SnarkUserCommandVerifyId, + }, + Error { + req_id: SnarkUserCommandVerifyId, + error: SnarkUserCommandVerifyError, + }, + #[action_event(level = info)] + Success { + req_id: SnarkUserCommandVerifyId, + }, + Finish { + req_id: SnarkUserCommandVerifyId, + }, +} + +impl redux::EnablingCondition for SnarkUserCommandVerifyAction { + fn is_enabled(&self, state: &crate::SnarkState, _time: redux::Timestamp) -> bool { + match self { + SnarkUserCommandVerifyAction::Init { + req_id, commands, .. + } => !commands.is_empty() && state.user_command_verify.jobs.next_req_id() == *req_id, + SnarkUserCommandVerifyAction::Pending { req_id } => state + .user_command_verify + .jobs + .get(*req_id) + .map_or(false, |v| v.is_init()), + SnarkUserCommandVerifyAction::Error { req_id, .. } => state + .user_command_verify + .jobs + .get(*req_id) + .map_or(false, |v| v.is_pending()), + SnarkUserCommandVerifyAction::Success { req_id } => state + .user_command_verify + .jobs + .get(*req_id) + .map_or(false, |v| v.is_pending()), + SnarkUserCommandVerifyAction::Finish { req_id } => state + .user_command_verify + .jobs + .get(*req_id) + .map_or(false, |v| v.is_finished()), + } + } +} diff --git a/snark/src/user_command_verify/snark_user_command_verify_effects.rs b/snark/src/user_command_verify/snark_user_command_verify_effects.rs new file mode 100644 index 000000000..ff5f0fcb8 --- /dev/null +++ b/snark/src/user_command_verify/snark_user_command_verify_effects.rs @@ -0,0 +1,33 @@ +use redux::ActionMeta; + +use super::{SnarkUserCommandVerifyAction, SnarkUserCommandVerifyService}; + +impl SnarkUserCommandVerifyAction { + pub fn effects(self, _: &ActionMeta, store: &mut Store) + where + Store: crate::SnarkStore, + Store::Service: SnarkUserCommandVerifyService, + SnarkUserCommandVerifyAction: redux::EnablingCondition, + { + match self { + SnarkUserCommandVerifyAction::Init { + req_id, commands, .. + } => { + let verifier_index = store.state().work_verify.verifier_index.clone(); + let verifier_srs = store.state().work_verify.verifier_srs.clone(); + store + .service() + .verify_init(req_id, commands, verifier_index, verifier_srs); + store.dispatch(SnarkUserCommandVerifyAction::Pending { req_id }); + } + SnarkUserCommandVerifyAction::Error { req_id, .. } => { + store.dispatch(SnarkUserCommandVerifyAction::Finish { req_id }); + } + SnarkUserCommandVerifyAction::Success { req_id } => { + store.dispatch(SnarkUserCommandVerifyAction::Finish { req_id }); + } + SnarkUserCommandVerifyAction::Pending { .. } => {} + SnarkUserCommandVerifyAction::Finish { .. } => {} + } + } +} diff --git a/snark/src/user_command_verify/snark_user_command_verify_reducer.rs b/snark/src/user_command_verify/snark_user_command_verify_reducer.rs new file mode 100644 index 000000000..7ec22ba16 --- /dev/null +++ b/snark/src/user_command_verify/snark_user_command_verify_reducer.rs @@ -0,0 +1,67 @@ +use super::{ + SnarkUserCommandVerifyAction, SnarkUserCommandVerifyActionWithMetaRef, + SnarkUserCommandVerifyState, SnarkUserCommandVerifyStatus, +}; + +impl SnarkUserCommandVerifyState { + pub fn reducer(&mut self, action: SnarkUserCommandVerifyActionWithMetaRef<'_>) { + let (action, meta) = action.split(); + match action { + SnarkUserCommandVerifyAction::Init { + commands, sender, .. + } => { + self.jobs.add(SnarkUserCommandVerifyStatus::Init { + time: meta.time(), + commands: commands.clone(), + sender: sender.clone(), + }); + } + SnarkUserCommandVerifyAction::Pending { req_id } => { + if let Some(req) = self.jobs.get_mut(*req_id) { + *req = match req { + SnarkUserCommandVerifyStatus::Init { + commands, sender, .. + } => SnarkUserCommandVerifyStatus::Pending { + time: meta.time(), + commands: std::mem::take(commands), + sender: std::mem::take(sender), + }, + _ => return, + }; + } + } + SnarkUserCommandVerifyAction::Error { req_id, error } => { + if let Some(req) = self.jobs.get_mut(*req_id) { + *req = match req { + SnarkUserCommandVerifyStatus::Pending { + commands, sender, .. + } => SnarkUserCommandVerifyStatus::Error { + time: meta.time(), + commands: std::mem::take(commands), + sender: std::mem::take(sender), + error: error.clone(), + }, + _ => return, + }; + } + } + SnarkUserCommandVerifyAction::Success { req_id } => { + if let Some(req) = self.jobs.get_mut(*req_id) { + *req = match req { + SnarkUserCommandVerifyStatus::Pending { + commands, sender, .. + } => SnarkUserCommandVerifyStatus::Success { + time: meta.time(), + commands: std::mem::take(commands), + sender: std::mem::take(sender), + }, + _ => return, + }; + } + } + SnarkUserCommandVerifyAction::Finish { req_id } => { + self.jobs.remove(*req_id); + } + } + } +} diff --git a/snark/src/user_command_verify/snark_user_command_verify_service.rs b/snark/src/user_command_verify/snark_user_command_verify_service.rs new file mode 100644 index 000000000..5fd7fc0c9 --- /dev/null +++ b/snark/src/user_command_verify/snark_user_command_verify_service.rs @@ -0,0 +1,17 @@ +use std::sync::{Arc, Mutex}; + +use ledger::scan_state::transaction_logic::{verifiable, WithStatus}; + +use crate::{VerifierIndex, VerifierSRS}; + +use super::SnarkUserCommandVerifyId; + +pub trait SnarkUserCommandVerifyService: redux::Service { + fn verify_init( + &mut self, + req_id: SnarkUserCommandVerifyId, + commands: Vec>, + verifier_index: Arc, + verifier_srs: Arc>, + ); +} diff --git a/snark/src/user_command_verify/snark_user_command_verify_state.rs b/snark/src/user_command_verify/snark_user_command_verify_state.rs new file mode 100644 index 000000000..71cf04893 --- /dev/null +++ b/snark/src/user_command_verify/snark_user_command_verify_state.rs @@ -0,0 +1,101 @@ +use std::sync::{Arc, Mutex}; + +use ledger::scan_state::transaction_logic::{verifiable, WithStatus}; +use serde::{Deserialize, Serialize}; + +use openmina_core::requests::PendingRequests; + +use crate::{VerifierIndex, VerifierSRS}; + +use super::{SnarkUserCommandVerifyError, SnarkUserCommandVerifyId, SnarkUserCommandVerifyIdType}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct SnarkUserCommandVerifyState { + pub verifier_index: Arc, + pub verifier_srs: Arc>, + pub jobs: PendingRequests, +} + +impl SnarkUserCommandVerifyState { + pub fn new(verifier_index: Arc, verifier_srs: Arc>) -> Self { + Self { + verifier_index, + verifier_srs, + jobs: Default::default(), + } + } + + pub fn next_req_id(&self) -> SnarkUserCommandVerifyId { + self.jobs.next_req_id() + } +} + +impl std::fmt::Debug for SnarkUserCommandVerifyState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SnarkUserCommandVerifyState") + // TODO(binier): display hashes instead. + .field("verifier_index", &"") + .field("verifier_srs", &"") + .field("jobs", &self.jobs) + .finish() + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum SnarkUserCommandVerifyStatus { + Init { + time: redux::Timestamp, + commands: Vec>, + // TODO(binier): move p2p/src/identity to shared crate and use + // `PeerId` here. + sender: String, + }, + Pending { + time: redux::Timestamp, + commands: Vec>, + sender: String, + }, + Error { + time: redux::Timestamp, + commands: Vec>, + sender: String, + error: SnarkUserCommandVerifyError, + }, + Success { + time: redux::Timestamp, + commands: Vec>, + sender: String, + }, +} + +impl SnarkUserCommandVerifyStatus { + pub fn is_init(&self) -> bool { + matches!(self, Self::Init { .. }) + } + + pub fn is_pending(&self) -> bool { + matches!(self, Self::Pending { .. }) + } + + pub fn is_finished(&self) -> bool { + matches!(self, Self::Error { .. } | Self::Success { .. }) + } + + pub fn commands(&self) -> &[WithStatus] { + match self { + Self::Init { commands, .. } => commands, + Self::Pending { commands, .. } => commands, + Self::Error { commands, .. } => commands, + Self::Success { commands, .. } => commands, + } + } + + pub fn sender(&self) -> &str { + match self { + Self::Init { sender, .. } => sender, + Self::Pending { sender, .. } => sender, + Self::Error { sender, .. } => sender, + Self::Success { sender, .. } => sender, + } + } +}