Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,747 changes: 2,567 additions & 1,180 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ void-oracle = { git = "ssh://git@github.com/essential-contributions/void-oracle.
void-oracle-types = { git = "ssh://git@github.com/essential-contributions/void-oracle.git" }
void-types = { git = "ssh://git@github.com/essential-contributions/void-base.git" }

proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
borsh = { version = "1.6", features = ["derive"] }

void-merkle = { path = "crates/merkle" }
void-network-channel = { path = "crates/network-channel" }
void-oracle-decoder = { path = "crates/void-oracle-decoder" }
void-proof = { path = "crates/proof" }
void-sign = { path = "crates/sign" }
void-solana-event = { path = "crates/solana-event" }
void-solana-event-derive = { path = "crates/solana-event-derive" }
4 changes: 2 additions & 2 deletions crates/proof/src/host/recursive.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::types::{RecursiveCommit, RecursiveType};
use risc0_zkvm::{ExecutorEnv, Receipt, default_prover};
use risc0_zkvm::{ExecutorEnv, ProverOpts, Receipt, default_prover};
use thiserror::Error;
use tracing::{debug, info};

Expand Down Expand Up @@ -129,7 +129,7 @@ where

debug!("running prover");
let receipt = prover
.prove(env, elf)
.prove_with_opts(env, elf, &ProverOpts::succinct())
.inspect_err(|e| tracing::error!("Failed to prove: {}", e))
.map_err(|e| RecursiveProofError::Proof(e.to_string()))?
.receipt;
Expand Down
17 changes: 17 additions & 0 deletions crates/solana-event-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "void-solana-event-derive"
version = "0.1.0"
authors.workspace = true
edition = "2021"
homepage.workspace = true
license.workspace = true
repository.workspace = true
description = "Derive macro for Solana events in void-toolkit"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
104 changes: 104 additions & 0 deletions crates/solana-event-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Derive macro for Solana events.
//!
//! This crate provides the `SolanaEvent` derive macro which generates:
//! - `DISCRIMINATOR` constant for event identification
//! - `emit()` method for emitting events from Solana programs
//! - `decode()` method for decoding events in applications
//! - `SolanaEventTrait` implementation

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

/// Derive macro for Solana events.
///
/// Generates event emission and decoding functionality for structs.
///
/// # Example
///
/// ```ignore
/// use borsh::{BorshDeserialize, BorshSerialize};
/// use void_solana_event::SolanaEvent;
///
/// #[derive(Debug, Clone, SolanaEvent, BorshSerialize, BorshDeserialize)]
/// pub struct AccountInitialized {
/// pub account_id: [u8; 32],
/// pub initial_value: u64,
/// }
///
/// // In a Solana program:
/// AccountInitialized {
/// account_id: [0u8; 32],
/// initial_value: 42,
/// }.emit()?;
///
/// // In an application:
/// let event = AccountInitialized::decode(&payload)?;
/// ```
#[proc_macro_derive(SolanaEvent)]
pub fn derive_solana_event(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let discriminator_str = format!("event:{}", name);

let expanded = quote! {
impl #name {
/// The event discriminator as a string slice.
pub const DISCRIMINATOR_STR: &'static str = #discriminator_str;

/// The event discriminator as bytes.
pub const DISCRIMINATOR: &'static [u8] = #discriminator_str.as_bytes();

/// Emit this event from a Solana program.
///
/// This method serializes the event using Borsh and emits it via `sol_log_data`.
/// The discriminator is sent as the first chunk, followed by the serialized payload.
///
/// On non-Solana targets, this is a no-op that always returns Ok(()).
pub fn emit(&self) -> Result<(), std::io::Error>
where
Self: borsh::BorshSerialize,
{
#[cfg(target_os = "solana")]
{
use borsh::BorshSerialize;
let mut data = Vec::new();
self.serialize(&mut data)?;
solana_program::log::sol_log_data(&[Self::DISCRIMINATOR, &data]);
}
Ok(())
}

/// Decode this event from Borsh-serialized bytes.
///
/// This is used on the application side to deserialize events.
pub fn decode(data: &[u8]) -> Result<Self, std::io::Error>
where
Self: borsh::BorshDeserialize,
{
use borsh::BorshDeserialize;
Self::try_from_slice(data)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
}

impl void_solana_event::SolanaEventTrait for #name {
fn discriminator() -> &'static [u8] {
Self::DISCRIMINATOR
}

fn discriminator_str() -> &'static str {
Self::DISCRIMINATOR_STR
}

fn decode_from_bytes(data: &[u8]) -> Result<Self, std::io::Error>
where
Self: Sized,
{
Self::decode(data)
}
}
};

TokenStream::from(expanded)
}
22 changes: 22 additions & 0 deletions crates/solana-event/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "void-solana-event"
version = "0.1.0"
authors.workspace = true
edition = "2021"
homepage.workspace = true
license.workspace = true
repository.workspace = true
description = "Solana event utilities for void-toolkit"

[dependencies]
base64.workspace = true
borsh = { version = "1.6", features = ["derive"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
void-solana-event-derive = { path = "../solana-event-derive" }

[target.'cfg(target_os = "solana")'.dependencies]
solana-program = "2.0"

[features]
default = []
219 changes: 219 additions & 0 deletions crates/solana-event/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
//! Solana event utilities for void-toolkit.
//! This crate provides:
//! - `SolanaEvent` derive macro for defining typed events
//! - Utilities for parsing Solana program logs
//! - Integration with void-oracle-decoder
//!
//! # Example
//!
//! ```ignore
//! use borsh::{BorshDeserialize, BorshSerialize};
//! use void_solana_event::SolanaEvent;
//!
//! #[derive(Debug, Clone, SolanaEvent, BorshSerialize, BorshDeserialize)]
//! pub struct AccountInitialized {
//! pub account_id: [u8; 32],
//! pub initial_value: u64,
//! }
//!
//! // In a Solana program:
//! AccountInitialized {
//! account_id: [0u8; 32],
//! initial_value: 42,
//! }.emit()?;
//! ```

pub use borsh::{BorshDeserialize, BorshSerialize};
pub use void_solana_event_derive::SolanaEvent;

/// Trait implemented by all Solana events via the derive macro.
///
/// This trait provides a common interface for event identification and decoding.
pub trait SolanaEventTrait: Sized + BorshDeserialize {
/// Returns the event discriminator bytes (e.g., `b"event:AccountInitialized"`).
fn discriminator() -> &'static [u8];

/// Returns the event discriminator as a string slice.
fn discriminator_str() -> &'static str;

/// Decode from Borsh-serialized bytes.
fn decode_from_bytes(data: &[u8]) -> Result<Self, std::io::Error>;
}

/// Parsed Solana transaction log data from void-oracle.
#[derive(Debug, Clone)]
pub struct SolanaLogData {
/// Transaction signature.
pub signature: String,
/// Slot number.
pub slot: u64,
/// Decoded event data from "Program data:" logs.
pub events: Vec<DecodedEventData>,
/// Error if transaction failed.
pub error: Option<serde_json::Value>,
}

/// A single decoded event from program logs.
#[derive(Debug, Clone)]
pub struct DecodedEventData {
/// The discriminator bytes (e.g., `b"event:AccountInitialized"`).
pub discriminator: Vec<u8>,
/// The Borsh-serialized payload bytes.
pub payload: Vec<u8>,
}

#[derive(serde::Deserialize)]
struct LogJson {
signature: String,
logs: Vec<String>,
slot: u64,
err: Option<serde_json::Value>,
}

/// Parse Solana program logs JSON into structured data.
///
/// This function parses the JSON format emitted by void-oracle for Solana program logs.
///
/// # Expected Input Format
///
/// ```json
/// {
/// "signature": "5abc...",
/// "logs": ["Program data: <base64> <base64>", ...],
/// "slot": 123,
/// "err": null
/// }
/// ```
///
/// # Returns
///
/// Returns a `SolanaLogData` struct containing parsed events from "Program data:" logs.
pub fn parse_solana_log_event(json_data: &[u8]) -> Result<SolanaLogData, std::io::Error> {
let log_json: LogJson = serde_json::from_slice(json_data)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

let events = extract_program_data(&log_json.logs)?;

Ok(SolanaLogData {
signature: log_json.signature,
slot: log_json.slot,
events,
error: log_json.err,
})
}

/// Extract and decode "Program data:" entries from Solana logs.
///
/// Each "Program data:" log contains space-separated base64 chunks:
/// - First chunk: event discriminator
/// - Remaining chunks: Borsh-serialized payload
fn extract_program_data(logs: &[String]) -> Result<Vec<DecodedEventData>, std::io::Error> {
use base64::Engine;
let mut events = Vec::new();

for log in logs {
if let Some(base64_data) = log.strip_prefix("Program data: ") {
// Each log can have multiple space-separated base64 chunks
// First chunk is discriminator, remaining chunks are payload
let chunks: Vec<Vec<u8>> = base64_data
.split_whitespace()
.filter_map(|chunk| base64::engine::general_purpose::STANDARD.decode(chunk).ok())
.collect();

if !chunks.is_empty() {
let discriminator = chunks[0].clone();
// Concatenate remaining chunks as payload
let payload: Vec<u8> = if chunks.len() > 1 {
chunks[1..].concat()
} else {
Vec::new()
};

events.push(DecodedEventData {
discriminator,
payload,
});
}
}
}

Ok(events)
}

/// Check if a discriminator matches a given event type's discriminator.
pub fn discriminator_matches<T: SolanaEventTrait>(discriminator: &[u8]) -> bool {
discriminator == T::discriminator()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_solana_log_event() {
use base64::Engine;

// Create mock log data
let discriminator = b"event:TestEvent";
let payload = vec![42u8, 0, 0, 0, 0, 0, 0, 0]; // u64 little-endian

let disc_b64 = base64::engine::general_purpose::STANDARD.encode(discriminator);
let payload_b64 = base64::engine::general_purpose::STANDARD.encode(&payload);

let json = format!(
r#"{{
"signature": "test_sig",
"logs": ["Program data: {} {}"],
"slot": 123,
"err": null
}}"#,
disc_b64, payload_b64
);

let result = parse_solana_log_event(json.as_bytes()).unwrap();

assert_eq!(result.signature, "test_sig");
assert_eq!(result.slot, 123);
assert_eq!(result.events.len(), 1);
assert_eq!(result.events[0].discriminator, discriminator);
assert_eq!(result.events[0].payload, payload);
}

#[test]
fn test_extract_program_data_multiple_events() {
use base64::Engine;

let disc1 = base64::engine::general_purpose::STANDARD.encode(b"event:Event1");
let payload1 = base64::engine::general_purpose::STANDARD.encode([1u8, 2, 3]);
let disc2 = base64::engine::general_purpose::STANDARD.encode(b"event:Event2");
let payload2 = base64::engine::general_purpose::STANDARD.encode([4u8, 5, 6]);

let logs = vec![
format!("Program data: {} {}", disc1, payload1),
"Program log: some other log".to_string(),
format!("Program data: {} {}", disc2, payload2),
];

let events = extract_program_data(&logs).unwrap();

assert_eq!(events.len(), 2);
assert_eq!(events[0].discriminator, b"event:Event1");
assert_eq!(events[0].payload, vec![1, 2, 3]);
assert_eq!(events[1].discriminator, b"event:Event2");
assert_eq!(events[1].payload, vec![4, 5, 6]);
}

#[test]
fn test_extract_program_data_no_payload() {
use base64::Engine;

let disc = base64::engine::general_purpose::STANDARD.encode(b"event:NoPayload");
let logs = vec![format!("Program data: {}", disc)];

let events = extract_program_data(&logs).unwrap();

assert_eq!(events.len(), 1);
assert_eq!(events[0].discriminator, b"event:NoPayload");
assert!(events[0].payload.is_empty());
}
}
Loading