From 9cf49c0cffb166a589e5f7f425331fcaa8e9e264 Mon Sep 17 00:00:00 2001 From: sebasti810 Date: Tue, 17 Dec 2024 10:04:32 +0100 Subject: [PATCH] fix: update build scripts since we need lumina-node types --- Cargo.lock | 1 + node-uniffi/Cargo.toml | 1 + node-uniffi/build-android.sh | 6 +- node-uniffi/build-ios.sh | 51 ++++++----- node-uniffi/src/error.rs | 72 ++++++++++++++++ node-uniffi/src/lib.rs | 159 +++++------------------------------ node-uniffi/src/types.rs | 153 ++++++++++++++++++++++++++++----- node/Cargo.toml | 7 +- node/src/network.rs | 3 +- 9 files changed, 270 insertions(+), 183 deletions(-) create mode 100644 node-uniffi/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 81543afc..9ab27d34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3560,6 +3560,7 @@ name = "lumina-node-uniffi" version = "0.1.0" dependencies = [ "celestia-types", + "directories", "libp2p", "lumina-node", "redb", diff --git a/node-uniffi/Cargo.toml b/node-uniffi/Cargo.toml index ea737524..05c5333e 100644 --- a/node-uniffi/Cargo.toml +++ b/node-uniffi/Cargo.toml @@ -19,5 +19,6 @@ libp2p.workspace = true redb = "2.1.1" thiserror = "1.0.61" serde_json = "1.0.64" +directories = "5.0.1" uniffi = { version = "0.28.3", features = ["bindgen", "tokio", "cli"] } tokio = { version = "1.38.0", features = ["macros", "sync"] } \ No newline at end of file diff --git a/node-uniffi/build-android.sh b/node-uniffi/build-android.sh index 335998e1..54c3273d 100755 --- a/node-uniffi/build-android.sh +++ b/node-uniffi/build-android.sh @@ -1,6 +1,6 @@ #!/bin/bash -cargo build -p native +cargo build -p lumina-node-uniffi rustup target add \ aarch64-linux-android \ @@ -16,4 +16,6 @@ cargo ndk -o ./app/src/main/jniLibs \ -t x86_64 \ build --release -cargo run --bin uniffi-bindgen generate --library ../target/debug/libnative.dylib --language kotlin --out-dir ./app/src/main/java/tech/forgen/native/rust \ No newline at end of file +cargo run --bin uniffi-bindgen generate --library ../target/debug/liblumina_node_uniffi.dylib --language kotlin --out-dir ./app/src/main/java/tech/forgen/lumina_node_uniffi/rust + +echo "Android build complete" \ No newline at end of file diff --git a/node-uniffi/build-ios.sh b/node-uniffi/build-ios.sh index 9cf73bce..fdc326dd 100755 --- a/node-uniffi/build-ios.sh +++ b/node-uniffi/build-ios.sh @@ -1,32 +1,37 @@ #!/bin/bash - -cargo build -p native -mkdir -p ./bindings -mkdir -p ./ios - -cargo run --bin uniffi-bindgen generate --library ../target/debug/libnative.dylib --language swift --out-dir ./bindings - +cd .. + for TARGET in \ - aarch64-apple-darwin \ aarch64-apple-ios \ - aarch64-apple-ios-sim \ - x86_64-apple-darwin \ - x86_64-apple-ios + aarch64-apple-ios-sim do rustup target add $TARGET cargo build --release --target=$TARGET done - -mv ./bindings/nativeFFI.modulemap ./bindings/module.modulemap - -rm ./ios/Native.swift -mv ./bindings/native.swift ./ios/Native.swift - -rm -rf "ios/Native.xcframework" + +cd node-uniffi + +rm -rf ./bindings ./ios +mkdir -p ./bindings +mkdir -p ./ios +mkdir -p ./bindings/Headers + +cargo run --bin uniffi-bindgen generate --library ../target/debug/liblumina_node_uniffi.dylib --language swift --out-dir ./bindings + +cat "./bindings/lumina_node_uniffiFFI.modulemap" "./bindings/lumina_nodeFFI.modulemap" > "./bindings/Headers/module.modulemap" + +cp ./bindings/*.h ./bindings/Headers/ + +rm -rf "ios/lumina.xcframework" + xcodebuild -create-xcframework \ - -library ../target/aarch64-apple-ios-sim/release/libnative.a -headers ./bindings \ - -library ../target/aarch64-apple-ios/release/libnative.a -headers ./bindings \ - -output "ios/Native.xcframework" - -rm -rf bindings \ No newline at end of file + -library ../target/aarch64-apple-ios-sim/release/liblumina_node_uniffi.a -headers ./bindings/Headers \ + -library ../target/aarch64-apple-ios/release/liblumina_node_uniffi.a -headers ./bindings/Headers \ + -output "ios/lumina.xcframework" + +cp ./bindings/*.swift ./ios/ + +rm -rf bindings + +echo "iOS build complete" \ No newline at end of file diff --git a/node-uniffi/src/error.rs b/node-uniffi/src/error.rs new file mode 100644 index 00000000..646188cf --- /dev/null +++ b/node-uniffi/src/error.rs @@ -0,0 +1,72 @@ +use lumina_node::NodeError; +use thiserror::Error; + +/// Result type alias for LuminaNode operations that can fail with a LuminaError +pub type Result = std::result::Result; + +/// Represents all possible errors that can occur in the LuminaNode. +#[derive(Error, Debug, uniffi::Error)] +pub enum LuminaError { + /// Error returned when trying to perform operations on a node that isn't running + #[error("Node is not running")] + NodeNotRunning, + + /// Error returned when network operations fail + #[error("Network error: {msg}")] + NetworkError { + /// Description of the network error + msg: String, + }, + + /// Error returned when storage operations fail + #[error("Storage error: {msg}")] + StorageError { + /// Description of the storage error + msg: String, + }, + + /// Error returned when trying to start a node that's already running + #[error("Node is already running")] + AlreadyRunning, + + /// Error returned when a mutex lock operation fails + #[error("Lock error")] + LockError, + + /// Error returned when a hash string is invalid or malformed + #[error("Invalid hash format: {msg}")] + InvalidHash { + /// Description of why the hash is invalid + msg: String, + }, + + /// Error returned when a header is invalid or malformed + #[error("Invalid header format: {msg}")] + InvalidHeader { + /// Description of why the header is invalid + msg: String, + }, + + /// Error returned when storage initialization fails + #[error("Storage initialization failed: {msg}")] + StorageInit { + /// Description of why storage initialization failed + msg: String, + }, +} + +impl From for LuminaError { + fn from(error: NodeError) -> Self { + LuminaError::NetworkError { + msg: error.to_string(), + } + } +} + +impl From for LuminaError { + fn from(e: libp2p::multiaddr::Error) -> Self { + LuminaError::NetworkError { + msg: format!("Invalid multiaddr: {}", e), + } + } +} diff --git a/node-uniffi/src/lib.rs b/node-uniffi/src/lib.rs index 442bc152..39ce6a3f 100644 --- a/node-uniffi/src/lib.rs +++ b/node-uniffi/src/lib.rs @@ -4,125 +4,29 @@ //! allowing it to be used from iOS and Android applications. #![cfg(not(target_arch = "wasm32"))] +mod error; mod types; use celestia_types::ExtendedHeader; -use libp2p::identity::Keypair; +use error::{LuminaError, Result}; use lumina_node::{ blockstore::RedbBlockstore, events::{EventSubscriber, TryRecvError}, network::Network, node::PeerTrackerInfo, store::RedbStore, - Node, NodeError, + Node, }; -use std::{path::PathBuf, str::FromStr, sync::Arc}; +use std::{str::FromStr, sync::Arc}; use tendermint::hash::Hash; -use thiserror::Error; use tokio::sync::Mutex; -use types::{NetworkInfo, NodeEvent, PeerId, SyncingInfo}; +use types::{NetworkInfo, NodeEvent, NodeStartConfig, PeerId, SyncingInfo}; use uniffi::Object; uniffi::setup_scaffolding!(); lumina_node::uniffi_reexport_scaffolding!(); -/// Result type alias for LuminaNode operations that can fail with a LuminaError -pub type Result = std::result::Result; - -/// Returns the platform-specific base path for storing Lumina data. -/// -/// The function determines the base path based on the target operating system: -/// - **iOS**: `~/Library/Application Support/lumina` -/// - **Android**: Value of the `LUMINA_DATA_DIR` environment variable -/// - **Other platforms**: Returns an error indicating unsupported platform. -fn get_base_path() -> Result { - #[cfg(target_os = "ios")] - { - std::env::var("HOME") - .map(PathBuf::from) - .map(|p| p.join("Library/Application Support/lumina")) - .map_err(|e| LuminaError::StorageError { - msg: format!("Could not get HOME directory: {}", e), - }) - } - - #[cfg(target_os = "android")] - { - std::env::var("LUMINA_DATA_DIR") - .map(PathBuf::from) - .map_err(|e| LuminaError::StorageError { - msg: format!("Could not get LUMINA_DATA_DIR: {}", e), - }) - } - - #[cfg(not(any(target_os = "ios", target_os = "android")))] - { - Err(LuminaError::StorageError { - msg: "Unsupported platform".to_string(), - }) - } -} - -/// Represents all possible errors that can occur in the LuminaNode. -#[derive(Error, Debug, uniffi::Error)] -pub enum LuminaError { - /// Error returned when trying to perform operations on a node that isn't running - #[error("Node is not running")] - NodeNotRunning, - - /// Error returned when network operations fail - #[error("Network error: {msg}")] - NetworkError { - /// Description of the network error - msg: String, - }, - - /// Error returned when storage operations fail - #[error("Storage error: {msg}")] - StorageError { - /// Description of the storage error - msg: String, - }, - - /// Error returned when trying to start a node that's already running - #[error("Node is already running")] - AlreadyRunning, - - /// Error returned when a mutex lock operation fails - #[error("Lock error")] - LockError, - - /// Error returned when a hash string is invalid or malformed - #[error("Invalid hash format: {msg}")] - InvalidHash { - /// Description of why the hash is invalid - msg: String, - }, - - /// Error returned when a header is invalid or malformed - #[error("Invalid header format: {msg}")] - InvalidHeader { - /// Description of why the header is invalid - msg: String, - }, - - /// Error returned when storage initialization fails - #[error("Storage initialization failed: {msg}")] - StorageInit { - /// Description of why storage initialization failed - msg: String, - }, -} - -impl From for LuminaError { - fn from(error: NodeError) -> Self { - LuminaError::NetworkError { - msg: error.to_string(), - } - } -} - /// The main Lumina node that manages the connection to the Celestia network. #[derive(Object)] pub struct LuminaNode { @@ -143,47 +47,30 @@ impl LuminaNode { })) } - /// Starts the Lumina node. Returns true if successfully started. + /// Start the node without optional configuration. + /// UniFFI needs explicit handling for optional parameters to generate correct bindings for different languages. pub async fn start(&self) -> Result { + self.start_with_config(None).await + } + + /// Start the node with specific configuration + pub async fn start_with_config(&self, config: Option) -> Result { let mut node_guard = self.node.lock().await; if node_guard.is_some() { return Err(LuminaError::AlreadyRunning); } - let network_id = self.network.id(); - - let base_path = get_base_path()?; - - std::fs::create_dir_all(&base_path).map_err(|e| LuminaError::StorageError { - msg: format!("Failed to create data directory: {}", e), - })?; + let config = config.unwrap_or_else(|| NodeStartConfig { + network: self.network.clone(), + bootnodes: None, + syncing_window_secs: None, + pruning_delay_secs: None, + batch_size: None, + ed25519_secret_key_bytes: None, + }); - let store_path = base_path.join(format!("store-{}", network_id)); - let db = Arc::new(redb::Database::create(&store_path).map_err(|e| { - LuminaError::StorageInit { - msg: format!("Failed to create database: {}", e), - } - })?); - - let store = RedbStore::new(db.clone()) - .await - .map_err(|e| LuminaError::StorageInit { - msg: format!("Failed to initialize store: {}", e), - })?; - - let blockstore = RedbBlockstore::new(db); - - let p2p_bootnodes = self.network.canonical_bootnodes().collect::>(); - let p2p_local_keypair = Keypair::generate_ed25519(); - - let builder = Node::builder() - .store(store) - .blockstore(blockstore) - .network(self.network.clone()) - .bootnodes(p2p_bootnodes) - .keypair(p2p_local_keypair) - .sync_batch_size(128); + let builder = config.into_node_builder().await?; let (new_node, subscriber) = builder .start_subscribed() @@ -191,8 +78,8 @@ impl LuminaNode { .map_err(|e| LuminaError::NetworkError { msg: e.to_string() })?; let mut events_guard = self.events_subscriber.lock().await; - *events_guard = Some(subscriber); + *events_guard = Some(subscriber); *node_guard = Some(new_node); Ok(true) } @@ -268,7 +155,7 @@ impl LuminaNode { let node = node_guard.as_ref().ok_or(LuminaError::NodeNotRunning)?; let peer_id = peer_id .to_libp2p() - .map_err(|e| LuminaError::NetworkError { msg: e })?; + .map_err(|e| LuminaError::NetworkError { msg: e.to_string() })?; Ok(node.set_peer_trust(peer_id, is_trusted).await?) } diff --git a/node-uniffi/src/types.rs b/node-uniffi/src/types.rs index 35db5040..6a568dc2 100644 --- a/node-uniffi/src/types.rs +++ b/node-uniffi/src/types.rs @@ -1,38 +1,153 @@ +use libp2p::identity::Keypair; use libp2p::swarm::ConnectionCounters as Libp2pConnectionCounters; use libp2p::swarm::NetworkInfo as Libp2pNetworkInfo; use libp2p::PeerId as Libp2pPeerId; use lumina_node::block_ranges::BlockRange as LuminaBlockRange; use lumina_node::events::{NodeEvent as LuminaNodeEvent, NodeEventInfo as LuminaNodeEventInfo}; use lumina_node::node::SyncingInfo as LuminaSyncingInfo; -use std::str::FromStr; -use std::time::SystemTime; +use lumina_node::{blockstore::RedbBlockstore, network, NodeBuilder}; +use std::sync::Arc; +use std::{ + path::PathBuf, + str::FromStr, + time::{Duration, SystemTime}, +}; use uniffi::Record; +use lumina_node::store::RedbStore; + +use crate::{error::Result, LuminaError}; + +#[cfg(target_os = "ios")] +use directories::ProjectDirs; + +#[cfg(target_os = "ios")] +/// Returns the platform-specific base path for storing on iOS. +fn get_base_path_impl() -> Result { + if let Some(proj_dirs) = ProjectDirs::from("com", "example", "Lumina") { + Ok(proj_dirs.data_dir().to_path_buf()) + } else { + Err(LuminaError::StorageError { + msg: "Could not determine a platform-specific data directory".to_string(), + }) + } +} + +#[cfg(target_os = "android")] +/// Returns the platform-specific base path for storing on Android. +/// +/// On Android, this function attempts to read the `LUMINA_DATA_DIR` environment variable. +/// If `LUMINA_DATA_DIR` is not set, it falls back to `/data/data/com.example.lumina/files`. +fn get_base_path_impl() -> Result { + match std::env::var("LUMINA_DATA_DIR") { + Ok(dir) => Ok(PathBuf::from(dir)), + Err(_) => { + let fallback = "/data/data/com.example.lumina/files"; + Ok(PathBuf::from(fallback)) + } + } +} + +#[cfg(not(any(target_os = "ios", target_os = "android")))] +/// Returns an error for unsupported platforms. +fn get_base_path_impl() -> Result { + Err(LuminaError::StorageError { + msg: "Unsupported platform".to_string(), + }) +} + +/// Returns the platform-specific base path for storing Lumina data. +/// +/// The function determines the base path based on the target operating system: +/// - **iOS**: `~/Library/Application Support/lumina` +/// - **Android**: Value of the `LUMINA_DATA_DIR` environment variable +/// - **Other platforms**: Returns an error indicating unsupported platform. +fn get_base_path() -> Result { + get_base_path_impl() +} + /// Configuration options for the Lumina node #[derive(Debug, Clone, Record)] pub struct NodeStartConfig { - /// Custom syncing window in seconds, defines maximum age of headers - /// considered for syncing and sampling + /// Network to connect to + pub network: network::Network, + /// Custom list of bootstrap peers to connect to. + /// If None, uses the canonical bootnodes for the network. + pub bootnodes: Option>, + /// Custom syncing window in seconds. Default is 30 days. pub syncing_window_secs: Option, - - /// Custom pruning delay after the syncing window in seconds + /// Custom pruning delay after syncing window in seconds. Default is 1 hour. pub pruning_delay_secs: Option, + /// Maximum number of headers in batch while syncing. Default is 128. + pub batch_size: Option, + /// Optional Set the keypair to be used as Node's identity. If None, generates a new Ed25519 keypair. + pub ed25519_secret_key_bytes: Option>, +} - /// Maximum number of headers in batch while syncing - pub sync_batch_size: Option, +impl NodeStartConfig { + /// Convert into NodeBuilder for the implementation + pub(crate) async fn into_node_builder(self) -> Result> { + let base_path = get_base_path()?; + let network_id = self.network.id(); + let store_path = base_path.join(format!("store-{}", network_id)); + std::fs::create_dir_all(&base_path).map_err(|e| LuminaError::StorageError { + msg: format!("Failed to create data directory: {}", e), + })?; + let db = Arc::new(redb::Database::create(&store_path).map_err(|e| { + LuminaError::StorageInit { + msg: format!("Failed to create database: {}", e), + } + })?); - /// Whether to listen for incoming connections - pub enable_listener: bool, -} + let store = RedbStore::new(db.clone()) + .await + .map_err(|e| LuminaError::StorageInit { + msg: format!("Failed to initialize store: {}", e), + })?; -impl Default for NodeStartConfig { - fn default() -> Self { - Self { - syncing_window_secs: None, - pruning_delay_secs: None, - sync_batch_size: None, - enable_listener: false, + let blockstore = RedbBlockstore::new(db); + + let bootnodes = if let Some(bootnodes) = self.bootnodes { + let mut resolved = Vec::with_capacity(bootnodes.len()); + for addr in bootnodes { + resolved.push(addr.parse()?); + } + resolved + } else { + self.network.canonical_bootnodes().collect::>() + }; + + let keypair = if let Some(key_bytes) = self.ed25519_secret_key_bytes { + if key_bytes.len() != 32 { + return Err(LuminaError::NetworkError { + msg: "Ed25519 private key must be 32 bytes".into(), + }); + } + + Keypair::ed25519_from_bytes(key_bytes).map_err(|e| LuminaError::NetworkError { + msg: format!("Invalid Ed25519 key: {}", e), + })? + } else { + libp2p::identity::Keypair::generate_ed25519() + }; + + let mut builder = NodeBuilder::new() + .store(store) + .blockstore(blockstore) + .network(self.network) + .bootnodes(bootnodes) + .keypair(keypair) + .sync_batch_size(self.batch_size.unwrap_or(128)); + + if let Some(secs) = self.syncing_window_secs { + builder = builder.sampling_window(Duration::from_secs(secs.into())); } + + if let Some(secs) = self.pruning_delay_secs { + builder = builder.pruning_delay(Duration::from_secs(secs.into())); + } + + Ok(builder) } } @@ -132,7 +247,7 @@ pub struct PeerId { } impl PeerId { - pub fn to_libp2p(&self) -> Result { + pub fn to_libp2p(&self) -> std::result::Result { Libp2pPeerId::from_str(&self.peer_id).map_err(|e| format!("Invalid peer ID format: {}", e)) } diff --git a/node/Cargo.toml b/node/Cargo.toml index ce04d344..6cf8af2e 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -18,6 +18,9 @@ categories = [ "wasm", ] +[lib] +crate-type = ["lib", "staticlib", "cdylib"] + [dependencies] celestia-proto.workspace = true celestia-types.workspace = true @@ -52,6 +55,8 @@ tokio-util = "0.7.11" tracing = "0.1.40" void = "1.0.2" web-time = "1.1.0" +uniffi = { version = "0.28.0", optional = true } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] backoff = { version = "0.4.0", features = ["tokio"] } @@ -69,7 +74,6 @@ libp2p = { workspace = true, features = [ redb = "2.1.1" rustls-pemfile = "2.1.2" rustls-pki-types = "1.7.0" -uniffi = { version = "0.28.0", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] backoff = { version = "0.4.0", features = ["wasm-bindgen"] } @@ -111,7 +115,6 @@ serde_json = "1.0.117" tempfile = "3.10.1" [features] -default-features = [] test-utils = ["celestia-types/test-utils"] uniffi = ["dep:uniffi", "celestia-types/uniffi"] diff --git a/node/src/network.rs b/node/src/network.rs index c57b2aca..969a21b5 100644 --- a/node/src/network.rs +++ b/node/src/network.rs @@ -32,7 +32,8 @@ pub struct InvalidNetworkId(String); #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct NetworkId { - pub id: String, // / Rename from 0 to id since uniffi doesn't support tuple structs + /// The network identifier string + pub id: String, } impl NetworkId {