From b442a8b182888781a95eb76f37645a3102fbe21d Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Tue, 3 Feb 2026 15:19:57 +0900 Subject: [PATCH 1/4] Track created service ids in accumulate context --- pvm/pvm-host/src/context/mod.rs | 4 ++++ pvm/pvm-host/src/host_functions/accumulate/mod.rs | 1 + pvm/pvm-host/src/host_functions/general/tests.rs | 1 + pvm/pvm-invocation/src/accumulate/mod.rs | 6 ++++++ 4 files changed, 12 insertions(+) diff --git a/pvm/pvm-host/src/context/mod.rs b/pvm/pvm-host/src/context/mod.rs index 4318b070..35c0df4c 100644 --- a/pvm/pvm-host/src/context/mod.rs +++ b/pvm/pvm-host/src/context/mod.rs @@ -188,6 +188,8 @@ pub struct AccumulateHostContext { pub yielded_accumulate_hash: Option, /// **`p`**: Provided preimage data pub provided_preimages: HashSet<(ServiceId, Octets)>, + /// Service ids created during this accumulation + pub created_service_ids: HashSet, /// Accumulate entry-point function invocation args (read-only) pub invoke_args: AccumulateInvokeArgs, /// Current entropy value (`η0′`) @@ -203,6 +205,7 @@ impl Clone for AccumulateHostContext { deferred_transfers: self.deferred_transfers.clone(), yielded_accumulate_hash: self.yielded_accumulate_hash.clone(), provided_preimages: self.provided_preimages.clone(), + created_service_ids: self.created_service_ids.clone(), invoke_args: self.invoke_args.clone(), curr_entropy: self.curr_entropy.clone(), } @@ -232,6 +235,7 @@ impl AccumulateHostContext { deferred_transfers: Vec::new(), yielded_accumulate_hash: None, provided_preimages: HashSet::new(), + created_service_ids: HashSet::new(), invoke_args, curr_entropy, }) diff --git a/pvm/pvm-host/src/host_functions/accumulate/mod.rs b/pvm/pvm-host/src/host_functions/accumulate/mod.rs index 3f31f72d..ebf7ff15 100644 --- a/pvm/pvm-host/src/host_functions/accumulate/mod.rs +++ b/pvm/pvm-host/src/host_functions/accumulate/mod.rs @@ -449,6 +449,7 @@ impl AccumulateHostFunction { x.rotate_new_account_id(state_provider).await?; new_service_id }; + x.created_service_ids.insert(new_service_id); tracing::debug!( "NEW service_id={new_service_id} parent={}", diff --git a/pvm/pvm-host/src/host_functions/general/tests.rs b/pvm/pvm-host/src/host_functions/general/tests.rs index 4a3881e2..a5cf323a 100644 --- a/pvm/pvm-host/src/host_functions/general/tests.rs +++ b/pvm/pvm-host/src/host_functions/general/tests.rs @@ -297,6 +297,7 @@ mod fetch_tests { deferred_transfers: vec![], yielded_accumulate_hash: None, provided_preimages: HashSet::new(), + created_service_ids: HashSet::new(), invoke_args: AccumulateInvokeArgs { inputs: self.accumulate_inputs.clone(), ..Default::default() diff --git a/pvm/pvm-invocation/src/accumulate/mod.rs b/pvm/pvm-invocation/src/accumulate/mod.rs index a73c7c7f..9a3b2d2c 100644 --- a/pvm/pvm-invocation/src/accumulate/mod.rs +++ b/pvm/pvm-invocation/src/accumulate/mod.rs @@ -48,6 +48,8 @@ pub struct AccumulateResult { pub gas_used: UnsignedGas, /// **`p`**: Provided preimage entries during accumulation pub provided_preimages: HashSet<(ServiceId, Octets)>, + /// Service ids created during accumulation + pub created_service_ids: HashSet, pub accumulate_host: ServiceId, } @@ -59,6 +61,7 @@ impl Default for AccumulateResult { yielded_accumulate_hash: None, gas_used: UnsignedGas::default(), provided_preimages: HashSet::new(), + created_service_ids: HashSet::new(), accumulate_host: ServiceId::default(), } } @@ -236,6 +239,7 @@ impl AccumulateInvocation { gas_used: result.gas_used, accumulate_host: x.accumulate_host, provided_preimages: x.provided_preimages, + created_service_ids: x.created_service_ids, })) } PVMInvocationOutput::OutputUnavailable => Ok(Some(AccumulateResult { @@ -245,6 +249,7 @@ impl AccumulateInvocation { gas_used: result.gas_used, accumulate_host: x.accumulate_host, provided_preimages: x.provided_preimages, + created_service_ids: x.created_service_ids, })), PVMInvocationOutput::OutOfGas(_) | PVMInvocationOutput::Panic(_) => { Ok(Some(AccumulateResult { @@ -254,6 +259,7 @@ impl AccumulateInvocation { gas_used: result.gas_used, // Note: taking gas usage from the `x` context accumulate_host: x.accumulate_host, provided_preimages: y.provided_preimages, + created_service_ids: y.created_service_ids, })) } } From 3ccc36faa38de975b8331f882b24115c7381e03c Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Tue, 3 Feb 2026 19:22:18 +0900 Subject: [PATCH 2/4] Track service ids generated in the current accumulation round and reject the block if duplicate service ids are generated --- pvm/pvm-invocation/src/accumulate/pipeline.rs | 41 +++++++++++-------- pvm/pvm-invocation/src/error.rs | 2 + 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/pvm/pvm-invocation/src/accumulate/pipeline.rs b/pvm/pvm-invocation/src/accumulate/pipeline.rs index aae786e7..e308834a 100644 --- a/pvm/pvm-invocation/src/accumulate/pipeline.rs +++ b/pvm/pvm-invocation/src/accumulate/pipeline.rs @@ -188,6 +188,7 @@ async fn merge_partial_state_change( prev_assigns: &AssignServices, prev_designate: ServiceId, prev_registrar: ServiceId, + created_service_ids: &HashSet, ) -> Result<(), PVMInvokeError> { // Accumulating service sandbox let accumulate_host_sandbox = partial_state_union @@ -306,33 +307,36 @@ async fn merge_partial_state_change( // Integrate new accounts and ejected accounts. // Note: no account other than the accumulate host is ever touched except for the // `NEW` & `EJECT` hostcalls. - accumulate_result_partial_state + for (&service_id, sandbox) in accumulate_result_partial_state .accounts_sandbox .iter() .filter(|(&service_id, _)| service_id != accumulate_host) - .for_each(|(&service_id, sandbox)| { - match sandbox.metadata.status() { - SandboxEntryStatus::Added => { - // Additional guard to avoid entries from the `partial_state_union` with `Added` - // status being copied into the later accumulations and overwriting any updates. - #[allow(clippy::map_entry)] - if !partial_state_union - .accounts_sandbox - .contains_key(&service_id) - { + { + match sandbox.metadata.status() { + SandboxEntryStatus::Added => { + match partial_state_union.accounts_sandbox.get(&service_id) { + None => { partial_state_union .accounts_sandbox .insert(service_id, sandbox.clone()); } + Some(_) => { + // If NEW service ids collide, block should be considered invalid. + if created_service_ids.contains(&service_id) { + return Err(PVMInvokeError::DuplicateNewServiceId(service_id)); + } + // Added entry from prior rounds; skip to avoid overwriting. + } } - SandboxEntryStatus::Removed => { - partial_state_union - .accounts_sandbox - .insert(service_id, sandbox.clone()); - } - _ => {} } - }); + SandboxEntryStatus::Removed => { + partial_state_union + .accounts_sandbox + .insert(service_id, sandbox.clone()); + } + _ => {} + } + } Ok(()) } @@ -487,6 +491,7 @@ async fn accumulate_parallel( &prev_assigns, prev_designate, prev_registrar, + &accumulate_result.created_service_ids, ) .await?; diff --git a/pvm/pvm-invocation/src/error.rs b/pvm/pvm-invocation/src/error.rs index e6ee798b..50537536 100644 --- a/pvm/pvm-invocation/src/error.rs +++ b/pvm/pvm-invocation/src/error.rs @@ -23,6 +23,8 @@ pub enum PVMInvokeError { WorkReportBlobTooLarge, #[error("Account sandbox for accumulate host (s={0}) is missing")] MissingAccumulateHostSandbox(ServiceId), + #[error("Duplicate new service id (s={0})")] + DuplicateNewServiceId(ServiceId), #[error("Number of work digests exceeds maximum")] WorkDigestsOverflow, #[error("Gas usage or gas limit overflowed")] From 365ae1faac4c6316077d989f236946532bba77f0 Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Wed, 4 Feb 2026 01:51:42 +0900 Subject: [PATCH 3/4] Allow the `check` function for new service IDs to reuse removed IDs --- pvm/pvm-host/src/context/partial_state.rs | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/pvm/pvm-host/src/context/partial_state.rs b/pvm/pvm-host/src/context/partial_state.rs index 73a33b52..2a5605db 100644 --- a/pvm/pvm-host/src/context/partial_state.rs +++ b/pvm/pvm-host/src/context/partial_state.rs @@ -278,7 +278,8 @@ impl AccountsSandboxMap { /// Returns `true` if the given service ID is already known, either in the accounts sandbox /// or in the global state. Even if the sandbox entry is marked for removed, returns `true`. - pub async fn account_exists_anywhere( + #[cfg(test)] + pub(crate) async fn account_exists_anywhere( &self, state_provider: Arc, service_id: ServiceId, @@ -290,6 +291,28 @@ impl AccountsSandboxMap { } } + /// Returns `true` if the account exists in the effective partial state: + /// - `Removed` entries are treated as non-existent. + /// - `Added`/`Updated`/`Clean` entries are treated as existing. + pub async fn account_exists_effective( + &self, + state_provider: Arc, + service_id: ServiceId, + ) -> Result { + if let Some(account_sandbox) = self.get(&service_id) { + if matches!( + account_sandbox.metadata.status(), + SandboxEntryStatus::Removed + ) { + Ok(false) + } else { + Ok(true) + } + } else { + Ok(state_provider.account_exists(service_id).await?) + } + } + /// Returns `true` only when the sandbox still has a live copy of the account: /// the entry exists and hasn’t been marked for removal, or it was newly added but /// not yet added to the global state. @@ -1303,7 +1326,7 @@ impl AccumulatePartialState { loop { if !self .accounts_sandbox - .account_exists_anywhere(state_provider.clone(), check_id) + .account_exists_effective(state_provider.clone(), check_id) .await? { return Ok(check_id); From a5ad86567046d7cf047c185366b529f73265a9e9 Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Wed, 4 Feb 2026 01:57:37 +0900 Subject: [PATCH 4/4] Service IDs removed from the partial state can be used for new service ID --- pvm/pvm-invocation/src/accumulate/pipeline.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pvm/pvm-invocation/src/accumulate/pipeline.rs b/pvm/pvm-invocation/src/accumulate/pipeline.rs index e308834a..d7cd426e 100644 --- a/pvm/pvm-invocation/src/accumulate/pipeline.rs +++ b/pvm/pvm-invocation/src/accumulate/pipeline.rs @@ -320,12 +320,18 @@ async fn merge_partial_state_change( .accounts_sandbox .insert(service_id, sandbox.clone()); } - Some(_) => { - // If NEW service ids collide, block should be considered invalid. - if created_service_ids.contains(&service_id) { + Some(existing) => { + if matches!(existing.metadata.status(), SandboxEntryStatus::Removed) { + // Allow re-creating a previously removed account. + partial_state_union + .accounts_sandbox + .insert(service_id, sandbox.clone()); + } else if created_service_ids.contains(&service_id) { + // If NEW service ids collide, block should be considered invalid. return Err(PVMInvokeError::DuplicateNewServiceId(service_id)); + } else { + // Inherited Added entry from a prior round; skip to avoid overwriting. } - // Added entry from prior rounds; skip to avoid overwriting. } } }