Skip to content

Commit b77006b

Browse files
committed
save place to check on first iteration of diffs
1 parent 4ba40d3 commit b77006b

File tree

12 files changed

+938
-13
lines changed

12 files changed

+938
-13
lines changed

Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ members = [
8787
"synthesizer/snark",
8888
"utilities",
8989
"utilities/derives",
90-
"wasm"
90+
"wasm",
91+
"plugins/slipstream_plugin_interface",
92+
"plugins/slipstream_plugin_manager"
9193
]
9294

9395
[lib]
@@ -389,6 +391,14 @@ default-features = false
389391
path = "ledger/store"
390392
version = "=4.5.0"
391393

394+
[workspace.dependencies.snarkvm-slipstream-plugin-interface]
395+
path = "plugins/slipstream_plugin_interface"
396+
version = "=4.5.0"
397+
398+
[workspace.dependencies.snarkvm-slipstream-plugin-manager]
399+
path = "plugins/slipstream_plugin_manager"
400+
version = "=4.5.0"
401+
392402
[workspace.dependencies.snarkvm-ledger-test-helpers]
393403
path = "ledger/test-helpers"
394404
version = "=4.5.0"

ledger/store/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ edition = "2024"
1818

1919
[features]
2020
default = [ "indexmap/rayon" ]
21-
history = [ ]
22-
history-staking-rewards = [ ]
21+
history = [ "dep:snarkvm-slipstream-plugin-manager" ]
22+
history-staking-rewards = [ "dep:snarkvm-slipstream-plugin-manager" ]
2323
locktick = [ "dep:locktick", "snarkvm-ledger-puzzle/locktick" ]
2424
rocks = [ "rocksdb", "smallvec" ]
2525
serial = [
@@ -42,6 +42,10 @@ wasm = [
4242
]
4343
test = [ ]
4444

45+
[dependencies.snarkvm-slipstream-plugin-manager]
46+
workspace = true
47+
optional = true
48+
4549
[dependencies.snarkvm-console]
4650
workspace = true
4751

ledger/store/src/program/finalize.rs

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ use std::{
3636
borrow::Cow,
3737
sync::atomic::{AtomicU32, Ordering},
3838
};
39+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
40+
use std::sync::{Arc, OnceLock, RwLock, atomic::AtomicBool};
41+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
42+
use snarkvm_slipstream_plugin_manager::SlipstreamPluginManager;
3943

4044
/// TODO (howardwu): Remove this.
4145
/// Returns the mapping ID for the given `program ID` and `mapping name`.
@@ -249,6 +253,7 @@ pub trait FinalizeStorage<N: Network>: 'static + Clone + Send + Sync {
249253
Ok(FinalizeOperation::InitializeMapping(to_mapping_id(&program_id, &mapping_name)?))
250254
}
251255

256+
// NOTE: THIS IS NEVER USED IN PROD
252257
/// Stores the given `(key, value)` pair at the given `program ID` and `mapping name` in storage.
253258
/// If the `mapping name` is not initialized, an error is returned.
254259
/// If the `key` already exists, the method returns an error.
@@ -279,6 +284,9 @@ pub trait FinalizeStorage<N: Network>: 'static + Clone + Send + Sync {
279284
// Update the historical maps.
280285
#[cfg(feature = "history")]
281286
{
287+
// TODO: TODO: Is here where we would want to stream the data?
288+
// NOTE: MAYBE WE WOULD WANT TO PUSH TO A BUFFER HERE AND THEN STREAM IT
289+
// IN ATOMIC_POST_RATIFY()
282290
let current_height = self.current_block_height().load(Ordering::SeqCst);
283291

284292
// Insert the initial value as the first historical update.
@@ -329,9 +337,10 @@ pub trait FinalizeStorage<N: Network>: 'static + Clone + Send + Sync {
329337
let value_id = N::hash_bhp1024(&(key_id, N::hash_bhp1024(&value.to_bits_le())?).to_bits_le())?;
330338

331339
atomic_batch_scope!(self, {
332-
// Update the historical maps.
340+
// Update the historical maps. // NOTE: THIS GETS CALLED IN vm/finalize.rs, TODO: STREAM HERE
333341
#[cfg(feature = "history")]
334342
{
343+
// TODO: Add to buffer here for optional streaming?
335344
let current_height = self.current_block_height().load(Ordering::SeqCst);
336345

337346
// Register the updated value at the current height.
@@ -654,6 +663,14 @@ pub struct FinalizeStore<N: Network, P: FinalizeStorage<N>> {
654663
storage: P,
655664
/// PhantomData.
656665
_phantom: PhantomData<N>,
666+
/// Indicates that canonical finalize is currently in progress.
667+
/// When `true`, storage writes notify registered Slipstream plugins.
668+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
669+
is_finalize_mode: Arc<AtomicBool>,
670+
/// Optional plugin manager for streaming canonical mapping and staking updates.
671+
/// Uses `OnceLock` so it can be installed from a shared reference after construction.
672+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
673+
slipstream_plugin_manager: OnceLock<Arc<RwLock<SlipstreamPluginManager>>>,
657674
}
658675

659676
impl<N: Network, P: FinalizeStorage<N>> FinalizeStore<N, P> {
@@ -665,7 +682,14 @@ impl<N: Network, P: FinalizeStorage<N>> FinalizeStore<N, P> {
665682
/// Initializes a finalize store from storage.
666683
pub fn from(storage: P) -> Result<Self> {
667684
// Return the finalize store.
668-
Ok(Self { storage, _phantom: PhantomData })
685+
Ok(Self {
686+
storage,
687+
_phantom: PhantomData,
688+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
689+
is_finalize_mode: Arc::new(AtomicBool::new(false)),
690+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
691+
slipstream_plugin_manager: OnceLock::new(),
692+
})
669693
}
670694

671695
/// Starts an atomic batch write operation.
@@ -714,6 +738,61 @@ impl<N: Network, P: FinalizeStorage<N>> FinalizeStore<N, P> {
714738
self.storage.current_block_height()
715739
}
716740

741+
/// Returns a reference to the canonical finalize mode flag.
742+
///
743+
/// When `true`, storage writes notify registered Slipstream plugins.
744+
/// Set to `true` by the VM before canonical finalize runs and reset to `false` afterwards.
745+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
746+
pub fn is_finalize_mode(&self) -> &Arc<AtomicBool> {
747+
&self.is_finalize_mode
748+
}
749+
750+
/// Installs a Slipstream plugin manager to receive canonical mapping and staking updates.
751+
///
752+
/// May be called from a shared reference. Logs a warning if called more than once.
753+
#[cfg(any(feature = "history", feature = "history-staking-rewards"))]
754+
pub fn set_slipstream_plugin_manager(&self, manager: Arc<RwLock<SlipstreamPluginManager>>) {
755+
if self.slipstream_plugin_manager.set(manager).is_err() {
756+
tracing::warn!("Slipstream plugin manager is already set; ignoring subsequent call.");
757+
}
758+
}
759+
760+
/// Notifies all interested plugins of a staking reward, if canonical finalize is active.
761+
///
762+
/// Errors from plugin calls are logged but never propagated.
763+
#[cfg(feature = "history-staking-rewards")]
764+
pub fn notify_staking_reward(
765+
&self,
766+
staker: &Address<N>,
767+
validator: &Address<N>,
768+
reward: u64,
769+
new_stake: u64,
770+
block_height: u32,
771+
) {
772+
if !self.is_finalize_mode.load(std::sync::atomic::Ordering::SeqCst) {
773+
return;
774+
}
775+
if let Some(mgr) = self.slipstream_plugin_manager.get() {
776+
let staker_bytes = match staker.to_bytes_le() {
777+
Ok(b) => b,
778+
Err(e) => {
779+
tracing::warn!("Slipstream: failed to serialize staker address: {e}");
780+
return;
781+
}
782+
};
783+
let validator_bytes = match validator.to_bytes_le() {
784+
Ok(b) => b,
785+
Err(e) => {
786+
tracing::warn!("Slipstream: failed to serialize validator address: {e}");
787+
return;
788+
}
789+
};
790+
mgr.read()
791+
.unwrap()
792+
.notify_staking_reward(&staker_bytes, &validator_bytes, reward, new_stake, block_height);
793+
}
794+
}
795+
717796
/// Returns the historical value of a mapping.
718797
#[cfg(feature = "history")]
719798
pub fn get_historical_mapping_value(
@@ -827,7 +906,32 @@ impl<N: Network, P: FinalizeStorage<N>> FinalizeStoreTrait<N> for FinalizeStore<
827906
key: Plaintext<N>,
828907
value: Value<N>,
829908
) -> Result<FinalizeOperation<N>> {
830-
self.storage.update_key_value(program_id, mapping_name, key, value)
909+
// Serialize before moving, if a plugin notification may be needed.
910+
#[cfg(feature = "history")]
911+
let plugin_data =
912+
if self.is_finalize_mode.load(Ordering::SeqCst) && self.slipstream_plugin_manager.get().is_some() {
913+
Some((
914+
program_id.to_bytes_le()?,
915+
mapping_name.to_bytes_le()?,
916+
key.to_bytes_le()?,
917+
value.to_bytes_le()?,
918+
))
919+
} else {
920+
None
921+
};
922+
923+
let result = self.storage.update_key_value(program_id, mapping_name, key, value)?;
924+
925+
// Notify plugins of the update if in canonical finalize mode.
926+
#[cfg(feature = "history")]
927+
if let Some((pid, mname, k, v)) = plugin_data {
928+
let height = self.storage.current_block_height().load(Ordering::SeqCst);
929+
if let Some(mgr) = self.slipstream_plugin_manager.get() {
930+
mgr.read().unwrap().notify_mapping_update(&pid, &mname, &k, &v, height);
931+
}
932+
}
933+
934+
Ok(result)
831935
}
832936

833937
/// Removes the key-value pair for the given `program ID`, `mapping name`, and `key` from storage.
@@ -860,7 +964,35 @@ impl<N: Network, P: FinalizeStorage<N>> FinalizeStore<N, P> {
860964
mapping_name: Identifier<N>,
861965
entries: Vec<(Plaintext<N>, Value<N>)>,
862966
) -> Result<FinalizeOperation<N>> {
863-
self.storage.replace_mapping(program_id, mapping_name, entries)
967+
// Serialize mapping identity and all entries before moving them into storage,
968+
// so they are available for plugin notification after the storage call.
969+
#[cfg(feature = "history")]
970+
let plugin_data: Option<(Vec<u8>, Vec<u8>, Vec<(Vec<u8>, Vec<u8>)>)> =
971+
if self.is_finalize_mode.load(Ordering::SeqCst) && self.slipstream_plugin_manager.get().is_some() {
972+
let mut serialized_entries = Vec::with_capacity(entries.len());
973+
for (key, value) in &entries {
974+
serialized_entries.push((key.to_bytes_le()?, value.to_bytes_le()?));
975+
}
976+
Some((program_id.to_bytes_le()?, mapping_name.to_bytes_le()?, serialized_entries))
977+
} else {
978+
None
979+
};
980+
981+
let result = self.storage.replace_mapping(program_id, mapping_name, entries)?;
982+
983+
// Notify plugins of each updated key-value pair if in canonical finalize mode.
984+
#[cfg(feature = "history")]
985+
if let Some((pid, mname, serialized_entries)) = plugin_data {
986+
let height = self.storage.current_block_height().load(Ordering::SeqCst);
987+
if let Some(mgr) = self.slipstream_plugin_manager.get() {
988+
let mgr_guard = mgr.read().unwrap();
989+
for (k, v) in &serialized_entries {
990+
mgr_guard.notify_mapping_update(&pid, &mname, k, v, height);
991+
}
992+
}
993+
}
994+
995+
Ok(result)
864996
}
865997

866998
/// Removes the mapping for the given `program ID` and `mapping name` from storage,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "snarkvm-slipstream-plugin-interface"
3+
version = "4.5.0"
4+
authors = [ "The Aleo Team <hello@aleo.org>" ]
5+
description = "The SnarkVM Slipstream plugin interface."
6+
homepage = "https://aleo.org"
7+
repository = "https://github.com/ProvableHQ/snarkVM"
8+
license = "Apache-2.0"
9+
edition = "2024"
10+
11+
[dependencies.anyhow]
12+
workspace = true
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Aleo Slipstream Plugin Interface
2+
3+
This crate enables a plugin to be added into a SnarkVM runtime to
4+
take actions at the time of mapping updates at block finalization;
5+
for example, saving historical mappings state and staking data to an external database. The plugin must
6+
implement the `SlipstreamPlugin` trait. Please see the details of the
7+
`slipstream_plugin_interface.rs` for the interface definition.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod slipstream_plugin_interface;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) 2019-2026 Provable Inc.
2+
// This file is part of the snarkVM library.
3+
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at:
7+
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
/// The interface for Aleo Slipstream plugins. A plugin must implement
17+
/// the `SlipstreamPlugin` trait to work with the runtime. In addition,
18+
/// the dynamic library must export a `C` function `_create_plugin` that
19+
/// creates the implementation of the plugin.
20+
use anyhow::Result;
21+
use std::any::Any;
22+
23+
pub trait SlipstreamPlugin: Any + Send + Sync + std::fmt::Debug {
24+
/// Returns the name of the plugin.
25+
fn name(&self) -> &'static str;
26+
27+
/// The callback called when a plugin is loaded by the system, used for
28+
/// doing whatever initialization is required by the plugin. The
29+
/// `_config_file` contains the name of the config file (JSON format) with
30+
/// a `libpath` field indicating the full path of the shared library.
31+
fn on_load(&mut self, _config_file: &str, _is_reload: bool) -> Result<()> {
32+
Ok(())
33+
}
34+
35+
/// The callback called right before a plugin is unloaded by the system.
36+
/// Used for doing cleanup before unload.
37+
fn on_unload(&mut self) {}
38+
39+
/// Called when a mapping key-value pair is inserted or updated during canonical finalize.
40+
/// All arguments are serialized to bytes to keep the trait object-safe.
41+
fn notify_mapping_update(
42+
&self,
43+
_program_id: &[u8],
44+
_mapping_name: &[u8],
45+
_key: &[u8],
46+
_value: &[u8],
47+
_block_height: u32,
48+
) -> Result<()> {
49+
Ok(())
50+
}
51+
52+
/// Called once per staker per block during staking reward distribution.
53+
fn notify_staking_reward(
54+
&self,
55+
_staker: &[u8],
56+
_validator: &[u8],
57+
_reward: u64,
58+
_new_stake: u64,
59+
_block_height: u32,
60+
) -> Result<()> {
61+
Ok(())
62+
}
63+
64+
/// Returns `true` if the plugin is interested in general mapping update data.
65+
fn history_enabled(&self) -> bool {
66+
false
67+
}
68+
69+
/// Returns `true` if the plugin is interested in staking reward data.
70+
fn history_staking_rewards_enabled(&self) -> bool {
71+
false
72+
}
73+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
name = "snarkvm-slipstream-plugin-manager"
3+
version = "4.5.0"
4+
authors = [ "The Aleo Team <hello@aleo.org>" ]
5+
description = "The SnarkVM Slipstream plugin manager."
6+
homepage = "https://aleo.org"
7+
repository = "https://github.com/ProvableHQ/snarkVM"
8+
license = "Apache-2.0"
9+
edition = "2024"
10+
11+
[dependencies.snarkvm-slipstream-plugin-interface]
12+
workspace = true
13+
14+
[dependencies.anyhow]
15+
workspace = true
16+
17+
[dependencies.thiserror]
18+
workspace = true
19+
20+
[dependencies.tracing]
21+
workspace = true
22+
23+
[dependencies.serde_json]
24+
workspace = true
25+
26+
[dependencies.libloading]
27+
version = "0.8"
28+
29+
[dependencies.json5]
30+
version = "0.4"
31+
32+
[dependencies.tokio]
33+
version = "1"
34+
features = [ "sync" ]

0 commit comments

Comments
 (0)