diff --git a/world-chain-builder/src/pbh/semaphore.rs b/world-chain-builder/src/pbh/semaphore.rs index db13c57a..a889882d 100644 --- a/world-chain-builder/src/pbh/semaphore.rs +++ b/world-chain-builder/src/pbh/semaphore.rs @@ -60,18 +60,18 @@ pub struct SemaphoreProof { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ExternalNullifier { - pub month: MonthMarker, + pub month: DateMarker, pub nonce: u16, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MonthMarker { +pub struct DateMarker { pub year: i32, pub month: u32, } impl ExternalNullifier { - pub fn new(month: MonthMarker, nonce: u16) -> Self { + pub fn new(month: DateMarker, nonce: u16) -> Self { Self { month, nonce } } } @@ -137,13 +137,13 @@ impl FromStr for ExternalNullifier { } } -impl MonthMarker { +impl DateMarker { pub fn new(year: i32, month: u32) -> Self { Self { year, month } } } -impl From for MonthMarker +impl From for DateMarker where T: Datelike, { @@ -155,8 +155,8 @@ where } } -impl From for NaiveDate { - fn from(value: MonthMarker) -> Self { +impl From for NaiveDate { + fn from(value: DateMarker) -> Self { NaiveDate::from_ymd_opt(value.year, value.month, 1).unwrap() } } @@ -173,7 +173,7 @@ pub enum MonthMarkerParsingError { InvalidYear(std::num::ParseIntError), } -impl FromStr for MonthMarker { +impl FromStr for DateMarker { type Err = MonthMarkerParsingError; fn from_str(s: &str) -> Result { @@ -193,11 +193,11 @@ impl FromStr for MonthMarker { return Err(MonthMarkerParsingError::MonthOutOfRange { month }); } - Ok(MonthMarker { year, month }) + Ok(DateMarker { year, month }) } } -impl std::fmt::Display for MonthMarker { +impl std::fmt::Display for DateMarker { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:02}{:04}", self.month, self.year) } @@ -252,7 +252,7 @@ mod test { #[test_case("022024")] #[test_case("022025")] fn parse_month_marker_roundtrip(s: &str) { - let m: MonthMarker = s.parse().unwrap(); + let m: DateMarker = s.parse().unwrap(); assert_eq!(m.to_string(), s); } @@ -263,6 +263,6 @@ mod test { #[test_case("" ; "empty")] #[test_case("23012024" ; "too long")] fn parse_month_marker_invalid(s: &str) { - s.parse::().unwrap_err(); + s.parse::().unwrap_err(); } } diff --git a/world-chain-builder/src/pool/validator.rs b/world-chain-builder/src/pool/validator.rs index b37a1763..5e476f6d 100644 --- a/world-chain-builder/src/pool/validator.rs +++ b/world-chain-builder/src/pool/validator.rs @@ -19,7 +19,7 @@ use super::ordering::WorldChainOrdering; use super::root::WorldChainRootValidator; use super::tx::{WorldChainPoolTransaction, WorldChainPooledTransaction}; use crate::pbh::db::{ExecutedPbhNullifierTable, ValidatedPbhTransactionTable}; -use crate::pbh::semaphore::{ExternalNullifier, MonthMarker, SemaphoreProof, TREE_DEPTH}; +use crate::pbh::semaphore::{DateMarker, ExternalNullifier, SemaphoreProof, TREE_DEPTH}; /// Type alias for World Chain transaction pool pub type WorldChainTransactionPool = Pool< @@ -104,8 +104,15 @@ where .map_err(WorldChainTransactionPoolInvalid::InvalidExternalNullifier) .map_err(TransactionValidationError::from)?; - let current_date = MonthMarker::from(date); - if external_nullifier.month != current_date { + // In most cases these will be the same value, but at the month boundary + // we'll still accept the previous month if the transaction is at most a minute late + // or the next month if the transaction is at most a minute early + let valid_dates = vec![ + DateMarker::from(date - chrono::Duration::minutes(1)), + DateMarker::from(date), + DateMarker::from(date + chrono::Duration::minutes(1)), + ]; + if valid_dates.iter().all(|d| external_nullifier.month != *d) { return Err(WorldChainTransactionPoolInvalid::InvalidExternalNullifierPeriod.into()); } @@ -285,9 +292,7 @@ mod tests { use test_case::test_case; use crate::pbh::db::load_world_chain_db; - use crate::pbh::semaphore::{ - ExternalNullifier, MonthMarker, Proof, SemaphoreProof, TREE_DEPTH, - }; + use crate::pbh::semaphore::{DateMarker, ExternalNullifier, Proof, SemaphoreProof, TREE_DEPTH}; use crate::pool::ordering::WorldChainOrdering; use crate::pool::root::{WorldChainRootValidator, LATEST_ROOT_SLOT, OP_WORLD_ID}; use crate::pool::tx::WorldChainPooledTransaction; @@ -346,7 +351,7 @@ mod tests { pbh_nonce: u16, ) -> SemaphoreProof { let external_nullifier = - ExternalNullifier::new(MonthMarker::from(time), pbh_nonce).to_string(); + ExternalNullifier::new(DateMarker::from(time), pbh_nonce).to_string(); create_proof(identity, external_nullifier, tx_hash, TREE_DEPTH) } @@ -631,8 +636,29 @@ mod tests { .unwrap(); } + #[test_case("v1-012025-0", "2024-12-31 23:59:30Z" ; "a minute early")] + #[test_case("v1-012025-0", "2025-02-01 00:00:30Z" ; "a minute late")] + fn validate_external_nullifier_at_time(external_nullifier: &str, time: &str) { + let validator = world_chain_validator(); + let date: chrono::DateTime = time.parse().unwrap(); + + let semaphore_proof = SemaphoreProof { + external_nullifier: external_nullifier.to_string(), + external_nullifier_hash: hash_to_field(external_nullifier.as_bytes()), + nullifier_hash: Field::ZERO, + signal_hash: Field::ZERO, + root: Field::ZERO, + proof: Default::default(), + }; + + validator + .validate_external_nullifier(date, &semaphore_proof) + .unwrap(); + } + #[test_case("v0-012025-0")] #[test_case("v1-022025-0")] + #[test_case("v1-122024-0")] #[test_case("v1-002025-0")] #[test_case("v1-012025-30")] #[test_case("v1-012025")] @@ -640,7 +666,7 @@ mod tests { #[test_case("v1-012025-0-0")] fn validate_external_nullifier_invalid(external_nullifier: &str) { let validator = world_chain_validator(); - let date = chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(); + let date = chrono::Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap(); let semaphore_proof = SemaphoreProof { external_nullifier: external_nullifier.to_string(),