Shekyl Stats

Refreshing...

Network

Connected
Seed Nodes Active
--

Chain

Current Block
0
Target Height
0
Top Block Hash
--
Block Time Target
2 min

Rewards

Last Block Reward
0.000000 SKL
Difficulty
0
Estimated Hash Rate
0 H/s

Supply

Circulating Supply
--
Remaining Supply
--
Total Burned
0.000000 SKL

Economics

Release Multiplier
0
Burn Rate %
0

Staking

Stake Ratio
0
Staker Pool
0.000000 SKL
Staker Emission Share
0
Total Staked
N/A
Staking Height
N/A
Tier 0 Lock Blocks
N/A
Tier 1 Lock Blocks
N/A
Tier 2 Lock Blocks
N/A

Protocol

Transaction Format
TransactionV3
Membership Proof
FCMP++
Spend Auth
Ed25519 + ML-DSA-65
Confidentiality
Stealth + BP+

Node

TX Pool Size
0
Database Size
0 B
Node Version
--
Sync Status
Syncing
All Documentation

Wallet RPC (Rust)

Rust-native wallet RPC interface: account management, transfers, and signing.

Rust Wallet RPC Design Document

Overview

The Rust wallet RPC layer replaces the C++ wallet_rpc_server with a Rust implementation that calls the existing C++ wallet2 library through a C FFI facade. This provides:

  • Standalone binary: shekyl-wallet-rpc, a drop-in replacement for the legacy C++ wallet RPC server
  • Embedded library: Linked directly into the Tauri GUI wallet for zero-overhead wallet operations without HTTP or process spawning

Architecture

┌─────────────────────────────────────────────────────────────┐
│                     GUI Wallet (Tauri)                       │
│  ┌───────────────┐  ┌──────────────┐  ┌──────────────────┐ │
│  │ wallet_bridge  │  │  commands    │  │  daemon_rpc      │ │
│  │ (direct FFI)   │  │  (Tauri cmds)│  │  (HTTP to daemon)│ │
│  └───────┬───────┘  └──────────────┘  └──────────────────┘ │
└──────────┼──────────────────────────────────────────────────┘
           │ (in-process Rust call)
┌──────────▼──────────────────────────────────────────────────┐
│              shekyl-wallet-rpc (Rust crate)                  │
│  ┌─────────┐  ┌──────────┐  ┌───────┐  ┌───────────────┐  │
│  │ wallet.rs│  │handlers.rs│  │server │  │ types.rs      │  │
│  │(Wallet2) │  │(dispatch) │  │(axum) │  │(serde structs)│  │
│  └────┬─────┘  └──────────┘  └───────┘  └───────────────┘  │
└───────┼─────────────────────────────────────────────────────┘
        │ (C FFI via ffi.rs)
┌───────▼─────────────────────────────────────────────────────┐
│              wallet2_ffi.cpp / wallet2_ffi.h                 │
│  ┌──────────────────────────────────────────────────────┐   │
│  │ wallet2_ffi_json_rpc() — generic JSON-RPC dispatcher │   │
│  │ + dedicated functions for lifecycle, transfer, etc.  │   │
│  └──────────────────────┬───────────────────────────────┘   │
└─────────────────────────┼───────────────────────────────────┘
                          │ (C++ method calls)
┌─────────────────────────▼───────────────────────────────────┐
│                    wallet2 (C++ core)                         │
│  Crypto · Sync · TX construction · Key management · LMDB    │
└──────────────────────────────────────────────────────────────┘

FFI Boundary Design

JSON at the boundary

Complex types cross the FFI as JSON strings serialized by RapidJSON on the C++ side and deserialized by serde on the Rust side. This avoids maintaining dozens of repr(C) structs and keeps the facade thin.

Simple scalar returns (height, bool, version) use direct C types.

Opaque handle

typedef struct wallet2_handle wallet2_handle;

wallet2_handle* wallet2_ffi_create(uint8_t nettype);
void wallet2_ffi_destroy(wallet2_handle* w);

The wallet2_handle wraps a tools::wallet2 instance plus last-error state. Rust wraps it in Wallet2 with a Drop impl that calls destroy.

Generic dispatcher

char* wallet2_ffi_json_rpc(wallet2_handle* w, const char* method, const char* params_json);

Routes any RPC method name to the corresponding wallet2 call. Returns a heap-allocated JSON string (caller frees with wallet2_ffi_free_string). On error, returns NULL and sets the handle's last-error fields.

Thread safety

wallet2 is not thread-safe. The Rust Wallet2 wrapper is Send but not Sync. Thread safety is enforced by wrapping it in std::sync::Mutex in the application state (both the axum server's AppState and the Tauri AppState).

Crate Structure

rust/shekyl-wallet-rpc/
├── Cargo.toml
└── src/
    ├── lib.rs               # Library entry point, re-exports
    ├── main.rs              # Standalone binary (clap CLI)
    ├── ffi.rs               # Raw C FFI bindings to wallet2_ffi.h
    ├── wallet.rs            # Safe Wallet2 wrapper
    ├── handlers.rs          # RPC dispatch (routes to wallet.json_rpc_call)
    ├── server.rs            # axum HTTP server + JSON-RPC routing
    ├── types.rs             # Request/response serde types
    ├── multisig_handlers.rs # FROST multisig RPC (feature = "multisig")
    └── scanner_state.rs     # Rust scanner state (feature = "rust-scanner")

GUI Wallet Integration

The GUI wallet (shekyl-gui-wallet) depends on shekyl-wallet-rpc as a Rust library. The integration layer is in wallet_bridge.rs:

  • WalletHandle: Mutex<Option<Wallet2>> — lazily initialized
  • init(): Creates a Wallet2 instance, connects to daemon
  • shutdown(): Drops the Wallet2 instance
  • Per-method functions: create_wallet(), get_balance(), transfer(), etc. — each acquires the mutex, calls the Wallet2 method, deserializes the JSON result into typed Rust structs

Build requirements

The GUI wallet's build.rs links against pre-built Shekyl C++ libraries.

Recommended: Using contrib/depends (CI builds, static linking)

This builds all third-party libraries (Boost, OpenSSL, etc.) from source with static linking. Produces a portable binary with no distro-versioned runtime dependencies.

# 1. Clone shekyl-core
git clone --recurse-submodules https://github.com/Shekyl-Foundation/shekyl-core.git ../Shekyl

# 2. Build everything via depends (downloads + builds all deps from source)
cd ../Shekyl && make depends target=x86_64-unknown-linux-gnu -j$(nproc)

# 3. Build the GUI wallet
cd shekyl-gui-wallet
export SHEKYL_BUILD_DIR=$(pwd)/../Shekyl/build/x86_64-unknown-linux-gnu/release
export SHEKYL_DEPENDS_PREFIX=$(pwd)/../Shekyl/contrib/depends/x86_64-unknown-linux-gnu
npx tauri build

Quick local development (dynamic linking)

For faster iteration when you already have system libraries installed:

# 1. Build shekyl-core with system libraries
cmake -S ../Shekyl -B ../Shekyl/build \
  -DCMAKE_BUILD_TYPE=Release \
  -DBUILD_SHARED_LIBS=OFF \
  -DBUILD_TESTS=OFF
cmake --build ../Shekyl/build -- -j$(nproc)

# 2. Build the GUI wallet (dynamic linking, no SHEKYL_DEPENDS_PREFIX)
export SHEKYL_BUILD_DIR=$(pwd)/../Shekyl/build
cd shekyl-gui-wallet/src-tauri
cargo build

Platform-specific dependencies (local dev only)

Linux (Ubuntu/Debian):

sudo apt-get install -y \
  build-essential cmake pkg-config \
  libboost-all-dev libssl-dev libunbound-dev \
  libsodium-dev libhidapi-dev libusb-1.0-0-dev \
  libprotobuf-dev protobuf-compiler libudev-dev

macOS (Homebrew):

brew install cmake boost hidapi openssl libpgm \
  miniupnpc expat protobuf abseil libsodium unbound

Windows: Not yet supported. The C++ codebase builds with MinGW but the Tauri toolchain requires MSVC-compatible .lib files. An MSVC compatibility investigation is in progress.

How build.rs works

The build script links 21 static libraries from the shekyl-core build tree plus the shekyl_ffi Rust FFI crate. It automatically derives the source directory (for the Rust FFI library) from SHEKYL_BUILD_DIR's parent. To override, set SHEKYL_SOURCE_DIR explicitly.

When SHEKYL_DEPENDS_PREFIX is set (pointing at the contrib/depends output), all third-party libraries (Boost, OpenSSL, sodium, etc.) are statically linked from that prefix. When unset, they are dynamically linked from system paths (suitable for local development).

If SHEKYL_BUILD_DIR is not set, the build succeeds but FFI functions are not linked — the wallet bridge will be available at the type level but calls will fail at link time. This allows CI lint/check passes without a full C++ build.

What was removed

FilePurposeReplacement
wallet_process.rsSpawn/manage shekyl-wallet-rpc child processDirect FFI via wallet_bridge.rs
wallet_rpc.rsHTTP JSON-RPC client to wallet-rpc processDirect FFI via wallet_bridge.rs

RPC Method Coverage

89 RPC methods from wallet_rpc_server.h are implemented in the wallet2_ffi_json_rpc dispatcher (9 classical multisig methods were removed; FROST multisig is handled by native Rust handlers, see below):

CategoryMethods
Lifecyclecreate_wallet, open_wallet, close_wallet, stop_wallet, store, change_wallet_password
Importrestore_deterministic_wallet, generate_from_keys
Balance/Addressget_balance, get_address, get_height, get_address_index
Accountsget_accounts, create_account, label_account, create_address, label_address
Account tagsget_account_tags, tag_accounts, untag_accounts, set_account_tag_description
Subaddressset_subaddress_lookahead
Transferstransfer, transfer_split, get_transfers, get_transfer_by_txid, incoming_transfers
Sweepssweep_all, sweep_single, sweep_dust/sweep_unmixable
Offline TXsign_transfer, describe_transfer, submit_transfer, relay_tx
Keys/Queriesquery_key, get_tx_key, sign, verify
Proofscheck_tx_key, get_tx_proof, check_tx_proof, get_reserve_proof, check_reserve_proof
Paymentsget_payments, get_bulk_payments
Address utilsmake_integrated_address, split_integrated_address, validate_address
URImake_uri, parse_uri
Address bookget_address_book, add_address_book, edit_address_book, delete_address_book
Export/Importexport_outputs, import_outputs, export_key_images, import_key_images
Freezefreeze, thaw, frozen
Attributesset_attribute, get_attribute, set_tx_notes, get_tx_notes
Refreshrefresh, auto_refresh, rescan_blockchain, rescan_spent, scan_tx
Miningstart_mining, stop_mining
Daemonset_daemon, set_log_level, set_log_categories
FROST Multisig (multisig feature)multisig_register_group, multisig_list_groups, multisig_create_signing, multisig_sign_preprocess, multisig_sign_add_preprocess, multisig_sign_nonce_sums, multisig_sign_own, multisig_sign_add_shares, multisig_sign_aggregate
Background syncsetup_background_sync, start_background_sync, stop_background_sync
Stakingstake, unstake, get_staked_outputs, get_staked_balance, claim_rewards
Feesestimate_tx_size_and_weight, get_default_fee_priority
Metaget_version, get_languages

Scanner Integration (rust-scanner feature)

When shekyl-wallet-rpc is compiled with --features rust-scanner, the RPC server uses split routing:

  • Scanner-backed methods are handled natively in Rust via shekyl-scanner: get_balance, get_transfers, incoming_transfers, get_transfer_by_txid, get_payments, get_bulk_payments, get_height, get_staked_outputs, get_staked_balance
  • All other methods continue routing through the C++ FFI

Architecture with Scanner

HTTP POST /json_rpc → handlers::dispatch_with_scanner(method)
  │
  ├── scanner-backed methods → shekyl-scanner (native Rust)
  │     WalletState, BalanceSummary, TransferDetails
  │
  ├── multisig_* methods → multisig_handlers (native Rust, "multisig" feature)
  │     MultisigState, MultisigSigningSession, MultisigGroup
  │
  └── remaining methods → C++ FFI (unchanged)
        transfer, sweep_*, sign_transfer, ...

Key Types

ModuleTypePurpose
shekyl-scannerScannerHybrid PQC KEM block/tx/output scan pipeline
shekyl-scannerRecoveredWalletOutputScan result with all KEM-derived secrets + key image
shekyl-scannerTransferDetailsExtended output with staking + PQC fields + eligible_height
shekyl-scannerWalletStateIn-memory transfer tracking, key image dedup, spend detection
shekyl-scannerBalanceSummaryStaking-aware balance breakdown (uses eligible_height)
shekyl-scanner::syncrun_sync_loopBackground daemon polling + scan + spend detection loop
shekyl-scanner::syncSyncProgressPer-block progress event (height, outputs found, spends)
shekyl-wallet-rpcScannerStateThread-safe wrapper around WalletState

GUI Integration

The Tauri GUI wallet's wallet_bridge.rs now includes a ScannerState alongside the Wallet2 handle. Query methods (get_scanner_balance, get_scanner_staked_outputs, get_scanner_height) read from the Rust scanner state. Mutation methods continue to use the C++ FFI.

FROST Multisig RPC (multisig feature)

When compiled with --features multisig, the RPC server provides native Rust FROST multisig endpoints. These bypass the C++ FFI entirely and are routed directly to multisig_handlers.rs in the json_rpc_handler.

Multisig state (MultisigState) is held in AppState under its own Mutex, separate from the wallet lock. This allows multisig operations to proceed without blocking wallet queries.

DKG is not exposed over RPC. The dkg-pedpop crate's round message types do not implement serde::Serialize/Deserialize, making direct RPC transport impractical. DKG is handled through the shekyl-wallet-core API with file-based message exchange (air-gap compatible). See docs/PQC_MULTISIG.md for the DKG ceremony flow.

Signing RPC methods:

MethodPurpose
multisig_register_groupRegister a MultisigGroup (threshold keys + PQC material)
multisig_list_groupsList registered group IDs
multisig_create_signingCreate a MultisigSigningSession for a set of inputs
multisig_sign_preprocessGenerate FROST commitments for the local participant
multisig_sign_add_preprocessAdd a remote participant's commitments
multisig_sign_nonce_sumsRetrieve aggregated nonce sums (hex bytes)
multisig_sign_ownProduce signing shares for the local participant
multisig_sign_add_sharesAdd a remote participant's signing shares
multisig_sign_aggregateAggregate all shares and produce the FCMP++ proof

All byte fields are hex-encoded in request/response JSON.

Sync Loop

The shekyl-scanner::sync module (feature-gated behind rust-scanner) implements the background sync loop:

pub async fn run_sync_loop<R, P, F>(
    rpc: R,                            // Rpc trait implementor
    scanner: Arc<Mutex<Scanner>>,      // Hybrid PQC KEM scanner
    state: Arc<Mutex<WalletState>>,    // In-memory wallet state
    cancel: CancellationToken,         // Graceful shutdown signal
    poll_interval: Duration,           // Sleep between tip polls
    flush_every_block: bool,           // Mobile: true; Desktop: false
    on_progress: P,                    // SyncProgress callback
    on_flush: F,                       // Persistence callback
) -> Result<(), SyncError>

The loop fetches blocks from wallet_state.height() + 1 up to the daemon tip via get_scannable_block_by_number, scans each block for wallet outputs, extracts key images from all block transactions for spend detection, and emits a SyncProgress event per block.

Reorg detection: Before processing each block, the loop compares the block's header.previous hash against the hash stored in WalletState for the prior height. On mismatch, it walks backwards (via find_fork_point) to locate the common ancestor, calls WalletState::handle_reorg to roll back affected transfers, flushes, and restarts scanning from the fork point.

Block fetch retries: Individual get_scannable_block_by_number calls retry up to 5 times with exponential backoff (500ms initial, 30s cap) before aborting. get_height retries indefinitely on each poll cycle.

Flush strategy:

  • Desktop: flush every 100 blocks plus at the tip.
  • Mobile: flush every block (OS can kill without warning).
  • Always flushes on reorg rollback and on cancellation before returning.

Cancellation: Dropping the CancellationToken or calling .cancel() causes the loop to finish the current block and return cleanly.

Future Work

  • FCMP++ signing in Rust: Transaction construction (FCMP++ proofs, PQC key signing) is the remaining major C++ dependency. Once implemented, the FFI layer can be removed entirely.
  • DKG RPC transport: If dkg-pedpop gains serde support (or a custom serialization layer is written), DKG round messages could be exposed over RPC for non-airgapped workflows.
  • GUI multisig integration: Wire the FROST multisig RPC endpoints into the Tauri wallet's wallet_bridge.rs for GUI-driven multisig signing.
  • Remove C++ wallet_rpc_server: Once the Rust RPC is proven in production, the C++ wallet_rpc_server.cpp and its epee HTTP infrastructure can be removed.