From 55423426028839f431bd81b2a0dde4b495da94fc Mon Sep 17 00:00:00 2001 From: Buffrr Date: Wed, 11 Mar 2026 00:44:53 +0100 Subject: [PATCH] Reuse snapshot for proving & remove all old proving functions --- .github/workflows/ci.yml | 2 + Cargo.lock | 23 -- Cargo.toml | 1 - client/Cargo.toml | 1 - client/src/bin/space-cli.rs | 240 +------------ client/src/rpc.rs | 565 ++---------------------------- client/src/store/chain.rs | 59 ++-- client/src/store/ptrs.rs | 35 +- client/src/store/spaces.rs | 35 +- client/src/wallets.rs | 29 -- client/tests/integration_tests.rs | 169 ++++++--- testutil/src/spaced.rs | 1 + 12 files changed, 222 insertions(+), 938 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d90a4f..1ef1faa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,11 @@ on: push: branches: - main + - subspaces pull_request: branches: - main + - subspaces jobs: run-tests: diff --git a/Cargo.lock b/Cargo.lock index 0ba2aa5..b9fb86e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,18 +675,6 @@ dependencies = [ "syn 2.0.115", ] -[[package]] -name = "domain" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c84070523f8ba0f9127ff156920f27eb27b302b425efe60bf5f41ec244d1c60" -dependencies = [ - "bytes", - "octseq", - "serde", - "time", -] - [[package]] name = "either" version = "1.15.0" @@ -1699,16 +1687,6 @@ dependencies = [ "libc", ] -[[package]] -name = "octseq" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" -dependencies = [ - "bytes", - "serde", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -2607,7 +2585,6 @@ dependencies = [ "clap", "colored", "directories", - "domain", "env_logger", "futures", "hex", diff --git a/Cargo.toml b/Cargo.toml index 28d5314..3afdcd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ directories = "5.0" threadpool = "1.8" tabled = "0.17" colored = "3.0" -domain = { version = "0.10", default-features = false, features = ["zonefile"] } tower = "0.4" hyper = "0.14" secp256k1 = "0.29" diff --git a/client/Cargo.toml b/client/Cargo.toml index 2c381a2..2338659 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -39,7 +39,6 @@ reqwest = { workspace = true } threadpool = { workspace = true } tabled = { workspace = true } colored = { workspace = true } -domain = { workspace = true } tower = { workspace = true } hyper = { workspace = true } diff --git a/client/src/bin/space-cli.rs b/client/src/bin/space-cli.rs index 3c13959..20470ef 100644 --- a/client/src/bin/space-cli.rs +++ b/client/src/bin/space-cli.rs @@ -2,18 +2,13 @@ extern crate core; use std::{ fs, io, - io::{Cursor, IsTerminal, Write}, + io::Write, path::PathBuf, }; use std::str::FromStr; use anyhow::anyhow; -use base64::Engine; use clap::{Parser, Subcommand}; use colored::{Color, Colorize}; -use domain::{ - base::{iana::Opcode, MessageBuilder, TreeCompressor}, - zonefile::inplace::{Entry, Zonefile}, -}; use jsonrpsee::{ core::{client::Error, ClientError}, http_client::HttpClient, @@ -39,7 +34,7 @@ use spaces_protocol::bitcoin::{Amount, FeeRate, OutPoint, Txid}; use spaces_protocol::slabel::SLabel; use spaces_ptr::snumeric::SNumeric; use spaces_ptr::sptr::Sptr; -use spaces_wallet::{bitcoin::secp256k1::schnorr::Signature, export::WalletExport, nostr::{NostrEvent, NostrTag}, Listing}; +use spaces_wallet::{bitcoin::secp256k1::schnorr::Signature, export::WalletExport, Listing}; use spaces_wallet::bitcoin::hashes::sha256; use spaces_wallet::bitcoin::ScriptBuf; @@ -337,50 +332,6 @@ enum Commands { #[arg(long)] seller: String, }, - /// Sign any Nostr event using the space's private key - #[command(name = "signevent")] - SignEvent { - /// Space name (e.g., @example) - space: String, - - /// Path to a Nostr event json file (omit for stdin) - #[arg(short, long)] - input: Option, - - /// Include a space-tag and trust path data - #[arg(short, long)] - anchor: bool, - }, - /// Verify a signed Nostr event against the space's public key - #[command(name = "verifyevent")] - VerifyEvent { - /// Space name (e.g., @example) - space: String, - - /// Path to a signed Nostr event json file (omit for stdin) - #[arg(short, long)] - input: Option, - }, - /// Sign a zone file turning it into a space-anchored Nostr event - #[command(name = "signzone")] - SignZone { - /// The space to use for signing the DNS file - space: String, - /// The DNS zone file path (omit for stdin) - input: Option, - /// Skip including bundled Merkle proof in the event. - #[arg(long)] - skip_anchor: bool, - }, - /// Updates the Merkle trust path for space-anchored Nostr events - #[command(name = "refreshanchor")] - RefreshAnchor { - /// Path to a Nostr event file (omit for stdin) - input: Option, - /// Prefer the most recent trust path (not recommended) - #[arg(long)] - prefer_recent: bool, - }, /// Get a spaceout - a Bitcoin output relevant to the Spaces protocol. #[command(name = "getspaceout")] GetSpaceOut { @@ -499,68 +450,6 @@ impl SpaceCli { )) } - async fn sign_event( - &self, - space: String, - event: NostrEvent, - anchor: bool, - most_recent: bool, - ) -> Result { - let subject = parse_subject(&space) - .map_err(|e| ClientError::Custom(format!("Invalid subject: {}", e)))?; - let mut result = self - .client - .wallet_sign_event(&self.wallet, subject, event) - .await?; - - if anchor { - result = self.add_anchor(result, most_recent).await? - } - - Ok(result) - } - async fn add_anchor( - &self, - mut event: NostrEvent, - most_recent: bool, - ) -> Result { - let space = match event.space() { - None => { - return Err(ClientError::Custom( - "A space tag is required to add an anchor".to_string(), - )) - } - Some(space) => space, - }; - - let spaceout = self - .client - .get_space(&space) - .await - .map_err(|e| ClientError::Custom(e.to_string()))? - .ok_or(ClientError::Custom(format!( - "Space not found \"{}\"", - space - )))?; - - event.proof = Some( - base64::prelude::BASE64_STANDARD.encode( - self.client - .prove_spaceout( - OutPoint { - txid: spaceout.txid, - vout: spaceout.spaceout.n as _, - }, - Some(most_recent), - ) - .await - .map_err(|e| ClientError::Custom(e.to_string()))? - .proof, - ), - ); - - Ok(event) - } async fn send_request( &self, req: Option, @@ -1014,84 +903,6 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client cli.client.verify_listing(listing).await?; println!("{} Listing verified", "✓".color(Color::Green)); } - Commands::SignEvent { - mut space, - input, - anchor, - } => { - let mut event = read_event(input) - .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - - space = normalize_space(&space); - match event.space() { - None if anchor => event - .tags - .insert(0, NostrTag(vec!["space".to_string(), space.clone()])), - Some(tag) => { - if tag != space { - return Err(ClientError::Custom(format!( - "Expected a space tag with value '{}', got '{}'", - space, tag - ))); - } - } - _ => {} - }; - - let result = cli.sign_event(space, event, anchor, false).await?; - println!("{}", serde_json::to_string(&result).expect("result")); - } - Commands::SignZone { - space, - input, - skip_anchor, - } => { - let update = encode_dns_update(&space, input) - .map_err(|e| ClientError::Custom(format!("Parse error: {}", e)))?; - let result = cli.sign_event(space, update, !skip_anchor, false).await?; - - println!("{}", serde_json::to_string(&result).expect("result")); - } - Commands::RefreshAnchor { - input, - prefer_recent, - } => { - let event = read_event(input) - .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - let space = match event.space() { - None => { - return Err(ClientError::Custom( - "Not a space-anchored event (no space tag)".to_string(), - )) - } - Some(space) => space, - }; - - let subject = parse_subject(&space) - .map_err(|e| ClientError::Custom(format!("Invalid subject: {}", e)))?; - let mut event = cli - .client - .verify_event(subject, event) - .await - .map_err(|e| ClientError::Custom(e.to_string()))?; - event.proof = None; - event = cli.add_anchor(event, prefer_recent).await?; - - println!("{}", serde_json::to_string(&event).expect("result")); - } - Commands::VerifyEvent { space, input } => { - let event = read_event(input) - .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - let subject = parse_subject(&space) - .map_err(|e| ClientError::Custom(format!("Invalid subject: {}", e)))?; - let event = cli - .client - .verify_event(subject, event) - .await - .map_err(|e| ClientError::Custom(e.to_string()))?; - - println!("{}", serde_json::to_string(&event).expect("result")); - } Commands::CreatePtr { spk, fee_rate } => { let spk = ScriptBuf::from(hex::decode(spk) .map_err(|_| ClientError::Custom("Invalid spk hex".to_string()))?); @@ -1257,50 +1068,3 @@ fn default_rpc_url(chain: &ExtendedNetwork) -> String { format!("http://127.0.0.1:{}", default_spaces_rpc_port(chain)) } -fn encode_dns_update(space: &str, zone_file: Option) -> anyhow::Result { - // domain crate panics if zone doesn't end in a new line - let zone = get_input(zone_file)? + "\n"; - - let mut builder = MessageBuilder::from_target(TreeCompressor::new(Vec::new()))?.authority(); - - builder.header_mut().set_opcode(Opcode::UPDATE); - - let mut cursor = Cursor::new(zone); - let mut reader = Zonefile::load(&mut cursor)?; - - while let Some(entry) = reader - .next_entry() - .or_else(|e| Err(anyhow!("Error reading zone entry: {}", e)))? - { - if let Entry::Record(record) = &entry { - builder.push(record)?; - } - } - - let msg = builder.finish(); - Ok(NostrEvent::new( - 871_222, - &base64::prelude::BASE64_STANDARD.encode(msg.as_slice()), - vec![NostrTag(vec!["space".to_string(), space.to_string()])], - )) -} - -fn read_event(file: Option) -> anyhow::Result { - let content = get_input(file)?; - let event: NostrEvent = serde_json::from_str(&content)?; - Ok(event) -} - -// Helper to handle file or stdin input -fn get_input(input: Option) -> anyhow::Result { - Ok(match input { - Some(file) => fs::read_to_string(file)?, - None => { - let input = io::stdin(); - match input.is_terminal() { - true => return Err(anyhow!("no input provided: specify file path or stdin")), - false => input.lines().collect::>()?, - } - } - }) -} diff --git a/client/src/rpc.rs b/client/src/rpc.rs index b56bc80..12311b6 100644 --- a/client/src/rpc.rs +++ b/client/src/rpc.rs @@ -40,7 +40,7 @@ use spaces_protocol::{ use spaces_wallet::{ bdk_wallet as bdk, bdk_wallet::template::Bip86, bitcoin::hashes::Hash as BitcoinHash, bitcoin::secp256k1::schnorr, - export::WalletExport, nostr::NostrEvent, Balance, DoubleUtxo, Listing, SpacesWallet, + export::WalletExport, Balance, DoubleUtxo, Listing, SpacesWallet, WalletConfig, WalletDescriptors, WalletOutput, }; pub use spaces_wallet::Subject; @@ -67,7 +67,7 @@ use crate::{ WalletCommand, WalletResponse, }, }; -use crate::store::chain::{Chain, COMMIT_BLOCK_INTERVAL, ROOT_ANCHORS_COUNT}; +use crate::store::chain::{Chain, COMMIT_BLOCK_INTERVAL, CACHED_SNAPSHOT_LOOKBACK}; use crate::store::Sha256; use crate::store::spaces::RolloutEntry; @@ -179,42 +179,12 @@ pub enum ChainStateCommand { listing: Listing, resp: Responder>, }, - VerifyEvent { - subject: Subject, - event: NostrEvent, - resp: Responder>, - }, VerifySchnorr { subject: Subject, message: Vec, signature: Vec, resp: Responder>, }, - ProveSpaceout { - outpoint: OutPoint, - prefer_recent: bool, - resp: Responder>, - }, - ProveSpaceOutpoint { - space_or_hash: String, - resp: Responder>, - }, - ProvePtrout { - outpoint: OutPoint, - prefer_recent: bool, - resp: Responder>, - }, - ProvePtrOutpoint { - subject: Subject, - prefer_recent: bool, - resp: Responder>, - }, - ProveCommitment { - space: SLabel, - root: Hash, - prefer_recent: bool, - resp: Responder>, - }, BuildChainProof { request: ChainProofRequest, prefer_recent: bool, @@ -316,20 +286,13 @@ pub trait Rpc { #[method(name = "walletimport")] async fn wallet_import(&self, wallet: WalletExport) -> Result<(), ErrorObjectOwned>; - #[method(name = "verifyevent")] - async fn verify_event( - &self, - subject: Subject, - event: NostrEvent, - ) -> Result; - #[method(name = "walletsignevent")] - async fn wallet_sign_event( + #[method(name = "walletcanoperate")] + async fn wallet_can_operate( &self, wallet: &str, - subject: Subject, - event: NostrEvent, - ) -> Result; + space: SLabel, + ) -> Result; #[method(name = "walletsignschnorr")] async fn wallet_sign_schnorr( @@ -339,13 +302,6 @@ pub trait Rpc { message: Bytes, ) -> Result; - #[method(name = "walletcanoperate")] - async fn wallet_can_operate( - &self, - wallet: &str, - space: SLabel, - ) -> Result; - #[method(name = "verifyschnorr")] async fn verify_schnorr( &self, @@ -410,41 +366,6 @@ pub trait Rpc { #[method(name = "verifylisting")] async fn verify_listing(&self, listing: Listing) -> Result<(), ErrorObjectOwned>; - #[method(name = "provespaceout")] - async fn prove_spaceout( - &self, - outpoint: OutPoint, - prefer_recent: Option, - ) -> Result; - - #[method(name = "provespaceoutpoint")] - async fn prove_space_outpoint( - &self, - space_or_hash: &str, - ) -> Result; - - #[method(name = "proveptrout")] - async fn prove_ptrout( - &self, - outpoint: OutPoint, - prefer_recent: Option, - ) -> Result; - - #[method(name = "proveptroutpoint")] - async fn prove_ptr_outpoint( - &self, - subject: Subject, - prefer_recent: Option, - ) -> Result; - - #[method(name = "provecommitment")] - async fn prove_commitment( - &self, - space: SLabel, - root: sha256::Hash, - prefer_recent: Option, - ) -> Result; - #[method(name = "buildchainproof")] async fn build_chain_proof( &self, @@ -607,17 +528,6 @@ pub struct RpcServerImpl { client: reqwest::Client, } -#[derive(Clone, Serialize, Deserialize)] -pub struct ProofResult { - pub root: Bytes, - #[serde( - serialize_with = "serialize_base64", - deserialize_with = "deserialize_base64" - )] - pub proof: Vec, -} - - /// Combined proof result for a chain proof request containing subtrees from both /// spaces and ptrs trees at the same snapshot height. @@ -1142,30 +1052,6 @@ impl RpcServer for RpcServerImpl { }) } - async fn verify_event( - &self, - subject: Subject, - event: NostrEvent, - ) -> Result { - self.store - .verify_event(subject, event) - .await - .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) - } - - async fn wallet_sign_event( - &self, - wallet: &str, - subject: Subject, - event: NostrEvent, - ) -> Result { - self.wallet(&wallet) - .await? - .send_sign_event(subject, event) - .await - .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) - } - async fn wallet_sign_schnorr( &self, wallet: &str, @@ -1316,61 +1202,6 @@ impl RpcServer for RpcServerImpl { .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } - async fn prove_spaceout( - &self, - outpoint: OutPoint, - prefer_recent: Option, - ) -> Result { - self.store - .prove_spaceout(outpoint, prefer_recent.unwrap_or(false)) - .await - .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) - } - - async fn prove_space_outpoint( - &self, - space_or_hash: &str, - ) -> Result { - self.store - .prove_space_outpoint(space_or_hash) - .await - .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) - } - - async fn prove_ptrout( - &self, - outpoint: OutPoint, - prefer_recent: Option, - ) -> Result { - self.store - .prove_ptrout(outpoint, prefer_recent.unwrap_or(false)) - .await - .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) - } - - async fn prove_ptr_outpoint( - &self, - subject: Subject, - prefer_recent: Option, - ) -> Result { - self.store - .prove_ptr_outpoint(subject, prefer_recent.unwrap_or(false)) - .await - .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) - } - - async fn prove_commitment( - &self, - space: SLabel, - root: sha256::Hash, - prefer_recent: Option, - ) -> Result { - self.store - .prove_commitment(space, *root.as_ref(), prefer_recent.unwrap_or(false)) - .await - .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) - } - async fn build_chain_proof( &self, request: ChainProofRequest, @@ -1721,10 +1552,6 @@ impl AsyncChainState { SpacesWallet::verify_listing::(state, &listing).map(|_| ()), ); } - ChainStateCommand::VerifyEvent { subject, event, resp } => { - let result = SpacesWallet::verify_event::(state, subject, event); - _ = resp.send(result); - } ChainStateCommand::VerifySchnorr { subject, message, signature, resp } => { let result = (|| { let sig = schnorr::Signature::from_slice(&signature) @@ -1733,59 +1560,6 @@ impl AsyncChainState { })(); _ = resp.send(result); } - ChainStateCommand::ProveSpaceout { - prefer_recent, - outpoint, - resp, - } => { - _ = resp.send(Self::handle_prove_spaceout( - state, - outpoint, - prefer_recent, - )); - } - ChainStateCommand::ProveSpaceOutpoint { - space_or_hash, - resp, - } => { - _ = resp.send(Self::handle_prove_space_outpoint( - state, - &space_or_hash, - )); - } - ChainStateCommand::ProvePtrout { - outpoint, - prefer_recent, - resp, - } => { - _ = resp.send(Self::handle_prove_ptrout( - state, - outpoint, - prefer_recent, - )); - } - ChainStateCommand::ProvePtrOutpoint { - subject, - prefer_recent, - resp, - } => { - let result = resolve_sptr(state, &subject) - .and_then(|sptr| Self::handle_prove_ptr_outpoint(state, sptr, prefer_recent)); - _ = resp.send(result); - } - ChainStateCommand::ProveCommitment { - space, - root, - prefer_recent, - resp, - } => { - _ = resp.send(Self::handle_prove_commitment( - state, - space, - root, - prefer_recent, - )); - } ChainStateCommand::BuildChainProof { request, prefer_recent, @@ -1871,202 +1645,11 @@ impl AsyncChainState { }]) } - fn handle_prove_space_outpoint( - state: &mut Chain, - space_or_hash: &str, - ) -> anyhow::Result { - let key = get_space_key(space_or_hash)?; - let snapshot = state.spaces_inner()?; - - // warm up hash cache - let root = snapshot.compute_root()?; - let proof = snapshot.prove(&[key.into()], ProofType::Standard)?; - - let buf = proof.to_vec()?; - - Ok(ProofResult { - proof: buf, - root: Bytes::new(root.to_vec()), - }) - } - - /// Determines the optimal snapshot block height for creating a Merkle proof. - /// - /// This function finds a suitable historical snapshot that: - /// 1. Is not older than when the space was last updated. - /// 2. Falls within [ROOT_ANCHORS_COUNT] range - /// 3. Skips the oldest trust anchors to prevent the proof from becoming stale too quickly. - /// - /// Parameters: - /// - last_update: Block height when the space was last updated - /// - tip: Current blockchain tip height - /// - /// Returns: Target block height aligned to [COMMIT_BLOCK_INTERVAL] - fn compute_target_snapshot(last_update: u32, tip: u32) -> u32 { - const SAFETY_MARGIN: u32 = 8; // Skip oldest trust anchors to prevent proof staleness - const USABLE_ANCHORS: u32 = ROOT_ANCHORS_COUNT - SAFETY_MARGIN; - - // Align block heights to commit intervals - let last_update_aligned = - last_update.div_ceil(COMMIT_BLOCK_INTERVAL) * COMMIT_BLOCK_INTERVAL; - let current_tip_aligned = (tip / COMMIT_BLOCK_INTERVAL) * COMMIT_BLOCK_INTERVAL; - - // Calculate the oldest allowed snapshot while maintaining safety margin - let lookback_window = (USABLE_ANCHORS - 1) * COMMIT_BLOCK_INTERVAL; - let oldest_allowed_snapshot = current_tip_aligned.saturating_sub(lookback_window); - - // Choose the most recent of last update or oldest allowed snapshot - // to ensure both data freshness and proof verifiability - std::cmp::max(last_update_aligned, oldest_allowed_snapshot) - } - - fn handle_prove_spaceout( - state: &mut Chain, - outpoint: OutPoint, - prefer_recent: bool, - ) -> anyhow::Result { - let key = OutpointKey::from_outpoint::(outpoint); - - let proof = if !prefer_recent { - let spaceout = match state.get_spaceout(&outpoint)? { - Some(spaceot) => spaceot, - None => { - return Err(anyhow!( - "Cannot find older proofs for a non-existent utxo (try with oldest: false)" - )) - } - }; - let target_snapshot = match spaceout.space.as_ref() { - None => return Ok(ProofResult { proof: vec![], root: Bytes::new(vec![]) }), - Some(space) => match space.covenant { - Covenant::Transfer { expire_height, .. } => { - let tip = state.tip(); - let last_update = expire_height.saturating_sub(spaces_protocol::constants::RENEWAL_INTERVAL); - Self::compute_target_snapshot(last_update, tip.height) - } - _ => return Err(anyhow!("Cannot find older proofs for a non-registered space (try with oldest: false)")), - } - }; - state.prove_spaces_with_snapshot(&[key.into()], target_snapshot)?.1 - } else { - let snapshot = state.spaces_inner()?; - snapshot.prove(&[key.into()], ProofType::Standard)? - }; - - let root = proof.compute_root()?.to_vec(); - info!("Proving with root anchor {}", hex::encode(root.as_slice())); - let buf = proof.to_vec()?; - - Ok(ProofResult { - proof: buf, - root: Bytes::new(root), - }) - } - - fn handle_prove_ptr_outpoint( - state: &mut Chain, - sptr: Sptr, - prefer_recent: bool, - ) -> anyhow::Result { - let key: Hash = sptr.into(); - - let proof = if !prefer_recent { - let ptr_info = match state.get_ptr_info(&sptr)? { - Some(info) => info, - None => { - return Err(anyhow!( - "Cannot find older proofs for a non-existent sptr (try with prefer_recent: true)" - )) - } - }; - let last_update = ptr_info.ptrout.sptr.last_update; - let tip = state.ptrs_tip(); - let target_snapshot = Self::compute_target_snapshot(last_update, tip.height); - state.prove_ptrs_with_snapshot(&[key], target_snapshot)?.1 - } else { - let snapshot = state.ptrs_mut().state.inner()?; - snapshot.prove(&[key], ProofType::Standard)? - }; - - let root = proof.compute_root()?.to_vec(); - info!("Proving SPTR with root anchor {}", hex::encode(root.as_slice())); - let buf = proof.to_vec()?; - - Ok(ProofResult { - proof: buf, - root: Bytes::new(root), - }) - } - - fn handle_prove_ptrout( - state: &mut Chain, - outpoint: OutPoint, - prefer_recent: bool, - ) -> anyhow::Result { - let key = PtrOutpointKey::from_outpoint::(outpoint); - - let proof = if !prefer_recent { - let ptrout = match state.get_ptrout(&outpoint)? { - Some(ptrout) => ptrout, - None => { - return Err(anyhow!( - "Cannot find older proofs for a non-existent utxo (try with prefer_recent: true)" - )) - } - }; - let tip = state.ptrs_tip(); - let target_snapshot = Self::compute_target_snapshot(ptrout.sptr.last_update, tip.height); - state.prove_ptrs_with_snapshot(&[key.into()], target_snapshot)?.1 - } else { - let snapshot = state.ptrs_mut().state.inner()?; - snapshot.prove(&[key.into()], ProofType::Standard)? - }; - - let root = proof.compute_root()?.to_vec(); - info!("Proving PTR with root anchor {}", hex::encode(root.as_slice())); - let buf = proof.to_vec()?; - - Ok(ProofResult { - proof: buf, - root: Bytes::new(root), - }) - } - - fn handle_prove_commitment( - state: &mut Chain, - space: SLabel, - root: Hash, - prefer_recent: bool, - ) -> anyhow::Result { - let key = CommitmentKey::new::(&space, root); - - let proof = if !prefer_recent { - let commitment = match state.get_commitment(&key)? { - Some(commitment) => commitment, - None => { - return Err(anyhow!( - "Cannot find older proofs for a non-existent commitment (try with prefer_recent: true)" - )) - } - }; - - // Use the block_height from the commitment to find an appropriate snapshot - let tip = state.ptrs_tip(); - let target_snapshot = Self::compute_target_snapshot(commitment.block_height, tip.height); - state.prove_ptrs_with_snapshot(&[key.into()], target_snapshot)?.1 - } else { - let snapshot = state.ptrs_mut().state.inner()?; - snapshot.prove(&[key.into()], ProofType::Standard)? - }; - - let root = proof.compute_root()?.to_vec(); - info!("Proving commitment with root anchor {}", hex::encode(root.as_slice())); - let buf = proof.to_vec()?; - - Ok(ProofResult { - proof: buf, - root: Bytes::new(root), - }) + /// Returns the height for a fixed cached snapshot (~1 week behind tip), + /// aligned to COMMIT_BLOCK_INTERVAL. Returns None if the chain is too young. + fn cached_snapshot_height(tip_height: u32) -> Option { + let tip_aligned = tip_height - (tip_height % COMMIT_BLOCK_INTERVAL); + tip_aligned.checked_sub(CACHED_SNAPSHOT_LOOKBACK) } fn handle_build_chain_proof( @@ -2153,8 +1736,30 @@ impl AsyncChainState { let ptr_tree_keys : Vec<_> = ptr_tree_keys.into_iter().collect(); let space_tree_keys : Vec<_> = space_tree_keys.into_iter().collect(); + let cached_height = Self::cached_snapshot_height(tip.height); + let use_cached = !prefer_recent + && cached_height.is_some_and(|h| most_recent_update <= h); + + let (spaces_proof, spaces_root, block_anchor, ptrs_proof, ptrs_root) = if use_cached { + let height = cached_height.unwrap(); + let snapshot = state.snapshot_at(height)?; + + let spaces_anchor: ChainAnchor = snapshot.spaces.metadata().try_into()?; + let spaces_proof = snapshot.spaces.prove(&space_tree_keys, ProofType::Standard)?; + let spaces_root = spaces_proof.compute_root()?; + + let ptrs_anchor: ChainAnchor = snapshot.ptrs.metadata().try_into()?; + if spaces_anchor != ptrs_anchor { + return Err(anyhow!( + "Spaces and PTRs snapshots at height {} have mismatched anchors", + height + )); + } + let ptrs_proof = snapshot.ptrs.prove(&ptr_tree_keys, ProofType::Standard)?; + let ptrs_root = ptrs_proof.compute_root()?; - let (spaces_proof, spaces_root, block_anchor, ptrs_proof, ptrs_root) = if prefer_recent { + (spaces_proof, spaces_root, spaces_anchor, ptrs_proof, ptrs_root) + } else { let spaces_snapshot = state.spaces_inner()?; let spaces_root = spaces_snapshot.compute_root()?; let spaces_anchor: ChainAnchor = spaces_snapshot.metadata().try_into()?; @@ -2172,23 +1777,6 @@ impl AsyncChainState { let ptrs_proof = ptrs_snapshot.prove(&ptr_tree_keys, ProofType::Standard)?; let ptrs_root = ptrs_proof.compute_root()?; - (spaces_proof, spaces_root, spaces_anchor, ptrs_proof, ptrs_root) - } else { - let target_snapshot = Self::compute_target_snapshot(most_recent_update, tip.height); - - let (spaces_anchor, spaces_proof) = state.prove_spaces_with_snapshot(&space_tree_keys, target_snapshot)?; - let spaces_root = spaces_proof.compute_root()?; - - let (ptrs_anchor, ptrs_proof) = state.prove_ptrs_with_snapshot(&ptr_tree_keys, target_snapshot)?; - let ptrs_root = ptrs_proof.compute_root()?; - - if spaces_anchor != ptrs_anchor { - return Err(anyhow!( - "Spaces and PTRs snapshots at height {} have mismatched anchors", - target_snapshot - )); - } - (spaces_proof, spaces_root, spaces_anchor, ptrs_proof, ptrs_root) }; @@ -2243,18 +1831,6 @@ impl AsyncChainState { resp_rx.await? } - pub async fn verify_event(&self, subject: Subject, event: NostrEvent) -> anyhow::Result { - let (resp, resp_rx) = oneshot::channel(); - self.sender - .send(ChainStateCommand::VerifyEvent { - subject, - event, - resp, - }) - .await?; - resp_rx.await? - } - pub async fn verify_schnorr( &self, subject: Subject, @@ -2273,79 +1849,6 @@ impl AsyncChainState { resp_rx.await? } - pub async fn prove_spaceout( - &self, - outpoint: OutPoint, - prefer_recent: bool, - ) -> anyhow::Result { - let (resp, resp_rx) = oneshot::channel(); - self.sender - .send(ChainStateCommand::ProveSpaceout { - outpoint, - prefer_recent: prefer_recent, - resp, - }) - .await?; - resp_rx.await? - } - - pub async fn prove_space_outpoint(&self, space_or_hash: &str) -> anyhow::Result { - let (resp, resp_rx) = oneshot::channel(); - self.sender - .send(ChainStateCommand::ProveSpaceOutpoint { - space_or_hash: space_or_hash.to_string(), - resp, - }) - .await?; - resp_rx.await? - } - - pub async fn prove_ptrout( - &self, - outpoint: OutPoint, - prefer_recent: bool, - ) -> anyhow::Result { - let (resp, resp_rx) = oneshot::channel(); - self.sender - .send(ChainStateCommand::ProvePtrout { - outpoint, - prefer_recent, - resp, - }) - .await?; - resp_rx.await? - } - - pub async fn prove_ptr_outpoint(&self, subject: Subject, prefer_recent: bool) -> anyhow::Result { - let (resp, resp_rx) = oneshot::channel(); - self.sender - .send(ChainStateCommand::ProvePtrOutpoint { - subject, - prefer_recent, - resp, - }) - .await?; - resp_rx.await? - } - - pub async fn prove_commitment( - &self, - space: SLabel, - root: Hash, - prefer_recent: bool, - ) -> anyhow::Result { - let (resp, resp_rx) = oneshot::channel(); - self.sender - .send(ChainStateCommand::ProveCommitment { - space, - root, - prefer_recent, - resp, - }) - .await?; - resp_rx.await? - } - pub async fn build_chain_proof( &self, request: ChainProofRequest, diff --git a/client/src/store/chain.rs b/client/src/store/chain.rs index 5046ce0..decc8f6 100644 --- a/client/src/store/chain.rs +++ b/client/src/store/chain.rs @@ -1,8 +1,7 @@ use std::path::Path; use anyhow::{anyhow, Context}; use log::info; -use spacedb::{Hash, Sha256Hasher}; -use spacedb::subtree::SubTree; +use spacedb::Hash; use spaces_protocol::bitcoin::{BlockHash, OutPoint}; use spaces_protocol::bitcoin::hashes::Hash as HashUtil; use spaces_protocol::constants::ChainAnchor; @@ -21,6 +20,8 @@ use crate::store::spaces::{RolloutEntry, RolloutIterator, SpLiveStore, SpStore, pub const ROOT_ANCHORS_COUNT: u32 = 120; pub const COMMIT_BLOCK_INTERVAL: u32 = 36; +/// ~1 week lookback for cached snapshot (7 * 144 = 1008 blocks) +pub const CACHED_SNAPSHOT_LOOKBACK: u32 = 1008; // https://internals.rust-lang.org/t/nicer-static-assertions/15986 macro_rules! const_assert { @@ -35,11 +36,28 @@ const_assert!( ); -#[derive(Clone)] +pub struct CachedSnapshot { + pub height: u32, + pub spaces: ReadTx, + pub ptrs: ReadTx, +} + pub struct Chain { db: LiveStore, idx: LiveIndex, ptrs_genesis: ChainAnchor, + cached_snapshot: Option, +} + +impl Clone for Chain { + fn clone(&self) -> Self { + Self { + db: self.db.clone(), + idx: self.idx.clone(), + ptrs_genesis: self.ptrs_genesis, + cached_snapshot: None, + } + } } #[derive(Clone)] @@ -99,6 +117,22 @@ impl Chain { self.db.pt.state.get_ptr_info(key) } + pub fn snapshot_at(&mut self, target_height: u32) -> anyhow::Result<&mut CachedSnapshot> { + if self.cached_snapshot.as_ref() + .is_some_and(|c| c.height > self.tip().height) + { + self.cached_snapshot = None; + } + + if !self.cached_snapshot.as_ref().is_some_and(|c| c.height == target_height) { + let spaces = self.db.sp.state.read_at(target_height)?; + let ptrs = self.db.pt.state.read_at(target_height)?; + self.cached_snapshot = Some(CachedSnapshot { height: target_height, spaces, ptrs }); + } + + Ok(self.cached_snapshot.as_mut().unwrap()) + } + pub fn load(_network: Network, genesis: ChainAnchor, ptrs_genesis: ChainAnchor, dir: &Path, index_spaces: bool, index_ptrs: bool) -> anyhow::Result { let proto_db_path = dir.join("root.sdb"); let ptrs_db_path = dir.join("refs.sdb"); @@ -134,7 +168,8 @@ impl Chain { let chain = Chain { db: LiveStore { sp, pt }, idx: LiveIndex { sp: sp_idx, pt: pt_idx }, - ptrs_genesis + ptrs_genesis, + cached_snapshot: None, }; // If spaces synced past the ptrs point, reset the tip @@ -340,22 +375,6 @@ impl Chain { self.db.sp.state.remove(key) } - pub fn prove_spaces_with_snapshot( - &self, - keys: &[Hash], - snapshot_block_height: u32, - ) -> anyhow::Result<(ChainAnchor, SubTree)> { - self.db.sp.state.prove_with_snapshot(keys, snapshot_block_height) - } - - pub fn prove_ptrs_with_snapshot( - &self, - keys: &[Hash], - snapshot_block_height: u32, - ) -> anyhow::Result<(ChainAnchor, SubTree)> { - self.db.pt.state.prove_with_snapshot(keys, snapshot_block_height) - } - pub fn get_spaces_block(&mut self, hash: BlockHash) -> anyhow::Result> { let idx = match &mut self.idx.sp { None => return Err(anyhow!("spaces index must be enabled")), diff --git a/client/src/store/ptrs.rs b/client/src/store/ptrs.rs index d6edc8a..3d80fd9 100644 --- a/client/src/store/ptrs.rs +++ b/client/src/store/ptrs.rs @@ -13,8 +13,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use spacedb::{ db::{Database, SnapshotIterator}, fs::FileBackend, - subtree::SubTree, - tx::{ProofType, ReadTransaction, WriteTransaction}, + tx::{ReadTransaction, WriteTransaction}, Configuration, Hash, Sha256Hasher, }; use spaces_protocol::{ @@ -188,30 +187,14 @@ impl PtrLiveSnapshot { }; } - pub fn prove_with_snapshot( - &self, - keys: &[Hash], - snapshot_block_height: u32, - ) -> Result<(ChainAnchor, SubTree)> { - let snapshot = self.db.iter().filter_map(|s| s.ok()).find(|s| { - let anchor: ChainAnchor = match s.metadata().try_into() { - Ok(a) => a, - _ => return false, - }; - anchor.height == snapshot_block_height - }); - if let Some(mut snapshot) = snapshot { - let anchor: ChainAnchor = snapshot.metadata().try_into() - .map_err(|_| anyhow!("Could not parse metadata"))?; - let proof = snapshot - .prove(keys, ProofType::Standard) - .or_else(|err| Err(anyhow!("Could not prove: {}", err)))?; - return Ok((anchor, proof)); - } - Err(anyhow!( - "Older snapshot targeting block {} could not be found", - snapshot_block_height - )) + pub fn read_at(&self, block_height: u32) -> anyhow::Result { + self.db.iter() + .filter_map(|s| s.ok()) + .find(|s| { + s.metadata().try_into() + .map_or(false, |a: ChainAnchor| a.height == block_height) + }) + .ok_or_else(|| anyhow!("Snapshot at block {} not found", block_height)) } pub fn inner(&mut self) -> anyhow::Result<&mut ReadTx> { diff --git a/client/src/store/spaces.rs b/client/src/store/spaces.rs index 7a3e3c0..81d75d8 100644 --- a/client/src/store/spaces.rs +++ b/client/src/store/spaces.rs @@ -16,8 +16,7 @@ use serde::Deserialize; use spacedb::{ db::{Database, SnapshotIterator}, fs::FileBackend, - subtree::SubTree, - tx::{KeyIterator, ProofType}, + tx::KeyIterator, Configuration, Hash, Sha256Hasher, }; use spaces_protocol::{ @@ -225,30 +224,14 @@ impl SpLiveSnapshot { }; } - pub fn prove_with_snapshot( - &self, - keys: &[Hash], - snapshot_block_height: u32, - ) -> Result<(ChainAnchor, SubTree)> { - let snapshot = self.db.iter().filter_map(|s| s.ok()).find(|s| { - let anchor: ChainAnchor = match s.metadata().try_into() { - Ok(a) => a, - _ => return false, - }; - anchor.height == snapshot_block_height - }); - if let Some(mut snapshot) = snapshot { - let anchor: ChainAnchor = snapshot.metadata().try_into() - .map_err(|_| anyhow!("Could not parse metadata"))?; - let proof = snapshot - .prove(keys, ProofType::Standard) - .or_else(|err| Err(anyhow!("Could not prove: {}", err)))?; - return Ok((anchor, proof)); - } - Err(anyhow!( - "Older snapshot targeting block {} could not be found", - snapshot_block_height - )) + pub fn read_at(&self, block_height: u32) -> anyhow::Result { + self.db.iter() + .filter_map(|s| s.ok()) + .find(|s| { + s.metadata().try_into() + .map_or(false, |a: ChainAnchor| a.height == block_height) + }) + .ok_or_else(|| anyhow!("Snapshot at block {} not found", block_height)) } pub fn inner(&mut self) -> anyhow::Result<&mut ReadTx> { diff --git a/client/src/wallets.rs b/client/src/wallets.rs index 4cb1346..585ccac 100644 --- a/client/src/wallets.rs +++ b/client/src/wallets.rs @@ -24,7 +24,6 @@ use spaces_wallet::{ bitcoin, bitcoin::{secp256k1::schnorr, Address, Amount, FeeRate, OutPoint}, builder::{CoinTransfer, SpaceTransfer, SpacesAwareCoinSelection}, - nostr::NostrEvent, tx_event::{TxEvent, TxEventKind, TxRecord}, Balance, DoubleUtxo, Listing, SpacesWallet, Subject, WalletInfo, WalletOutput, }; @@ -314,11 +313,6 @@ pub enum WalletCommand { resp: crate::rpc::Responder>, }, UnloadWallet, - SignEvent { - subject: Subject, - event: NostrEvent, - resp: crate::rpc::Responder>, - }, SignSchnorr { subject: Subject, message: Vec, @@ -645,13 +639,6 @@ impl RpcWallet { WalletCommand::Sell { space, price, resp } => { _ = resp.send(wallet.sell::(chain, &space, Amount::from_sat(price))); } - WalletCommand::SignEvent { - subject, - event, - resp, - } => { - _ = resp.send(wallet.sign_event::(chain, subject, event)); - } WalletCommand::SignSchnorr { subject, message, @@ -1820,22 +1807,6 @@ impl RpcWallet { resp_rx.await? } - pub async fn send_sign_event( - &self, - subject: Subject, - event: NostrEvent, - ) -> anyhow::Result { - let (resp, resp_rx) = oneshot::channel(); - self.sender - .send(WalletCommand::SignEvent { - subject, - event, - resp, - }) - .await?; - resp_rx.await? - } - pub async fn send_list_transactions( &self, count: usize, diff --git a/client/tests/integration_tests.rs b/client/tests/integration_tests.rs index 327c75f..3ed54cc 100644 --- a/client/tests/integration_tests.rs +++ b/client/tests/integration_tests.rs @@ -5,16 +5,18 @@ use spaces_client::{ BidParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest, RpcWalletTxBuilder, Subject, TransferSpacesParams, }, + store::chain::{CACHED_SNAPSHOT_LOOKBACK, COMMIT_BLOCK_INTERVAL}, wallets::{AddressKind, WalletResponse}, }; use spaces_protocol::{ bitcoin::{Amount, FeeRate}, constants::RENEWAL_INTERVAL, slabel::SLabel, - Covenant, + Bytes, Covenant, }; +use spaces_ptr::ChainProofRequest; use spaces_testutil::TestRig; -use spaces_wallet::{export::WalletExport, nostr::NostrEvent, tx_event::TxEventKind}; +use spaces_wallet::{export::WalletExport, tx_event::TxEventKind}; const ALICE: &str = "wallet_99"; const BOB: &str = "wallet_98"; @@ -1214,43 +1216,6 @@ async fn it_should_allow_buy_sell(rig: &TestRig) -> anyhow::Result<()> { Ok(()) } -async fn it_should_allow_sign_verify_messages(rig: &TestRig) -> anyhow::Result<()> { - rig.wait_until_wallet_synced(BOB).await.expect("synced"); - - let alice_spaces = rig - .spaced - .client - .wallet_list_spaces(BOB) - .await - .expect("bob spaces"); - let space = alice_spaces - .owned - .first() - .expect("bob should have at least 1 space"); - - let space_name = space.spaceout.space.as_ref().unwrap().name.to_string(); - - let msg = NostrEvent::new(1, "hello world", vec![]); - let space_or_ptr = Subject::Space(SLabel::from_str(&space_name).unwrap()); - let signed = rig - .spaced - .client - .wallet_sign_event(BOB, space_or_ptr.clone(), msg.clone()) - .await - .expect("sign"); - - println!("signed\n{}", serde_json::to_string_pretty(&signed).unwrap()); - assert_eq!(signed.content, msg.content, "msg content must match"); - - rig.spaced - .client - .verify_event(space_or_ptr, signed.clone()) - .await - .expect("verify"); - - Ok(()) -} - async fn it_should_handle_expired_spaces(rig: &TestRig) -> anyhow::Result<()> { rig.wait_until_wallet_synced(ALICE).await?; rig.wait_until_synced().await?; @@ -1419,6 +1384,123 @@ async fn it_should_handle_expired_spaces(rig: &TestRig) -> anyhow::Result<()> { Ok(()) } +async fn it_should_sign_and_verify_schnorr(rig: &TestRig) -> anyhow::Result<()> { + rig.wait_until_wallet_synced(BOB).await?; + + let spaces = rig.spaced.client.wallet_list_spaces(BOB).await?; + let space = spaces.owned.first().expect("bob should have at least 1 space"); + let space_name = space.spaceout.space.as_ref().unwrap().name.to_string(); + let subject = Subject::Space(SLabel::from_str(&space_name).unwrap()); + + let message = Bytes::new(b"hello world".to_vec()); + let signature = rig + .spaced + .client + .wallet_sign_schnorr(BOB, subject.clone(), message.clone()) + .await?; + + assert!(!signature.is_empty(), "signature should not be empty"); + + let valid = rig + .spaced + .client + .verify_schnorr(subject, message, signature) + .await?; + assert!(valid, "signature must be valid"); + + Ok(()) +} + +async fn it_should_build_chain_proof_with_snapshot_caching(rig: &TestRig) -> anyhow::Result<()> { + rig.wait_until_wallet_synced(ALICE).await?; + rig.wait_until_synced().await?; + + let spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; + let space = spaces.owned.first().expect("alice should have spaces"); + let space_name = space.spaceout.space.as_ref().unwrap().name.to_string(); + let label = SLabel::from_str(&space_name).unwrap(); + + // Use debug RPC to set expire_height far in the future so last_update is old + // last_update = expire_height - RENEWAL_INTERVAL, so setting expire_height + // to RENEWAL_INTERVAL + 1 makes last_update = 1 (very old) + let old_expire_height = RENEWAL_INTERVAL + 1; + rig.spaced.client.debug_set_expire_height(&space_name, old_expire_height).await?; + + // Mine to next commit boundary so the change is committed + let tip = rig.get_block_count().await? as u32; + let remaining = tip % COMMIT_BLOCK_INTERVAL; + if remaining > 0 { + rig.mine_blocks((COMMIT_BLOCK_INTERVAL - remaining) as usize, None).await?; + } + rig.wait_until_synced().await?; + + let tip = rig.get_block_count().await? as u32; + let cached_height = (tip - tip % COMMIT_BLOCK_INTERVAL) + .saturating_sub(CACHED_SNAPSHOT_LOOKBACK); + let last_update = old_expire_height.saturating_sub(RENEWAL_INTERVAL); + assert!( + last_update <= cached_height, + "last_update ({}) should be <= cached_height ({})", + last_update, cached_height + ); + + // Prove the space - last_update=1 <= cached_height, should use cached snapshot + let proof1 = rig + .spaced + .client + .build_chain_proof( + ChainProofRequest { + spaces: vec![label.clone()], + ptrs_keys: vec![], + }, + Some(false), + ) + .await?; + + assert!(!proof1.spaces_proof.is_empty(), "proof should not be empty"); + assert_eq!( + proof1.block.height, cached_height, + "old space should use cached snapshot (last_update={}, cached_height={})", + last_update, cached_height + ); + + // Now set expire_height so last_update is recent (above cached_height) + let recent_expire_height = tip + RENEWAL_INTERVAL; + rig.spaced.client.debug_set_expire_height(&space_name, recent_expire_height).await?; + + // Mine to next commit boundary + let tip2 = rig.get_block_count().await? as u32; + let remaining = tip2 % COMMIT_BLOCK_INTERVAL; + if remaining > 0 { + rig.mine_blocks((COMMIT_BLOCK_INTERVAL - remaining) as usize, None).await?; + } + rig.wait_until_synced().await?; + + // Prove again - last_update is now recent, should use live state + let proof2 = rig + .spaced + .client + .build_chain_proof( + ChainProofRequest { + spaces: vec![label.clone()], + ptrs_keys: vec![], + }, + Some(false), + ) + .await?; + + let tip3 = rig.get_block_count().await? as u32; + let cached_height2 = (tip3 - tip3 % COMMIT_BLOCK_INTERVAL) + .saturating_sub(CACHED_SNAPSHOT_LOOKBACK); + assert!(!proof2.spaces_proof.is_empty(), "proof should not be empty"); + assert!( + proof2.block.height > cached_height2, + "updated space should use live state, not cached snapshot" + ); + + Ok(()) +} + async fn it_should_handle_reorgs(rig: &TestRig) -> anyhow::Result<()> { rig.wait_until_wallet_synced(ALICE).await.expect("synced"); const NAME: &str = "hello_world"; @@ -1481,15 +1563,16 @@ async fn run_auction_tests() -> anyhow::Result<()> { it_should_allow_buy_sell(&rig) .await .expect("should allow buy sell"); - it_should_allow_sign_verify_messages(&rig) + it_should_sign_and_verify_schnorr(&rig) .await - .expect("should sign verify"); - + .expect("should sign and verify schnorr"); + it_should_build_chain_proof_with_snapshot_caching(&rig) + .await + .expect("should build chain proof with snapshot caching"); it_should_handle_expired_spaces(&rig) .await .expect("should handle expired spaces"); - // keep reorgs last as it can drop some txs from mempool and mess up wallet state it_should_handle_reorgs(&rig) .await diff --git a/testutil/src/spaced.rs b/testutil/src/spaced.rs index 8ec7786..7d9d238 100644 --- a/testutil/src/spaced.rs +++ b/testutil/src/spaced.rs @@ -60,6 +60,7 @@ impl SpaceD { } }; + #[allow(deprecated)] let child = std::process::Command::cargo_bin("spaced")? .args(args) .arg("--rpc-port")