diff --git a/Cargo.lock b/Cargo.lock index f9522d43..2294a302 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3625,7 +3625,7 @@ dependencies = [ [[package]] name = "l2l-openapi" version = "0.1.0" -source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=caa8c1c248aca1827b57007dcfcd8ff70d607e8e#caa8c1c248aca1827b57007dcfcd8ff70d607e8e" +source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=a97f7923ce4b62587aebc7561d713bb2ebc89e52#a97f7923ce4b62587aebc7561d713bb2ebc89e52" dependencies = [ "jsonrpsee", "l2l-openapi-macros", @@ -3635,7 +3635,7 @@ dependencies = [ [[package]] name = "l2l-openapi-macros" version = "0.1.0" -source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=caa8c1c248aca1827b57007dcfcd8ff70d607e8e#caa8c1c248aca1827b57007dcfcd8ff70d607e8e" +source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=a97f7923ce4b62587aebc7561d713bb2ebc89e52#a97f7923ce4b62587aebc7561d713bb2ebc89e52" dependencies = [ "proc-macro2", "proc_macro_roids", @@ -6643,12 +6643,15 @@ dependencies = [ name = "thunder_app_rpc_api" version = "0.17.5" dependencies = [ + "anyhow", "bitcoin", + "const-hex", "jsonrpsee", "l2l-openapi", "serde", "serde_json", "thunder_types", + "typewit", "utoipa", ] @@ -7164,6 +7167,7 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" dependencies = [ + "serde", "typewit_proc_macros", ] diff --git a/Cargo.toml b/Cargo.toml index 589eaa0d..c5865097 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ tracing-appender = "0.2.3" tracing-indicatif = "0.3.8" tracing-subscriber = "0.3.20" transitive = "1.0.1" +typewit = { version = "1.15", default-features = false } utoipa = { version = "5.2.0", default-features = false } url = { version = "2.5.4", default-features = false } uuid = "1.13.1" @@ -96,7 +97,7 @@ path = "bip300301_enforcer/integration_tests" [workspace.dependencies.l2l-openapi] git = "https://github.com/Ash-L2L/l2l-openapi" -rev = "caa8c1c248aca1827b57007dcfcd8ff70d607e8e" +rev = "a97f7923ce4b62587aebc7561d713bb2ebc89e52" [workspace.dependencies.rustreexo] git = "https://github.com/mit-dci/rustreexo" diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 7ec881ec..988abbcf 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -7,12 +7,12 @@ use jsonrpsee::{ types::ErrorObject, }; use thunder::types::{ - Address, Block, Pointed, PointedOutput, SpentOutput, Txid, + Address, Block, M6id, Pointed, PointedOutput, SpentOutput, Txid, WithdrawalBundle, net::{Peer, PeerAddress}, wallet::Balance, }; -use thunder_app_rpc_api as rpc_api; +use thunder_app_rpc_api::{self as rpc_api, typewit::const_marker::Bool}; use tower_http::{ cors::CorsLayer, request_id::{ @@ -117,6 +117,78 @@ impl rpc_api::node::PrivateRpcServer for RpcServerImpl { } } +#[async_trait] +impl + rpc_api::node::get_block::RpcServer> + for RpcServerImpl +{ + async fn get_block( + &self, + block_hash: thunder::types::BlockHash, + _verbose: Bool, + ) -> RpcResult< + Option< as rpc_api::node::get_block::Verbosity>::Response>, + > { + let Some(header) = self + .app + .node + .try_get_header(block_hash) + .map_err(custom_err)? + else { + return Ok(None); + }; + let body = self.app.node.get_body(block_hash).map_err(custom_err)?; + let block = thunder::types::Block { header, body }; + Ok(Some(block)) + } +} + +#[async_trait] +impl + rpc_api::node::get_block::RpcServer> + for RpcServerImpl +{ + async fn get_block( + &self, + block_hash: thunder::types::BlockHash, + _verbose: Bool, + ) -> RpcResult< + Option< as rpc_api::node::get_block::Verbosity>::Response>, + > { + let Some(header) = self + .app + .node + .try_get_header(block_hash) + .map_err(custom_err)? + else { + return Ok(None); + }; + let thunder::types::Body { + coinbase, + transactions, + authorizations, + } = self.app.node.get_body(block_hash).map_err(custom_err)?; + let transactions_verbose = transactions + .into_iter() + .map(|tx| { + Ok(thunder_app_rpc_api::node::TransactionVerbose { + canonical_bytes: tx.canonical_bytes()?, + tx, + }) + }) + .collect::>() + .map_err(custom_err)?; + let body = thunder_app_rpc_api::node::get_block::BodyVerbose { + coinbase, + transactions: transactions_verbose, + authorizations, + }; + let block_verbose = + thunder_app_rpc_api::node::get_block::BlockVerbose { header, body }; + Ok(Some(block_verbose)) + } +} + #[async_trait] impl rpc_api::node::RpcServer for RpcServerImpl @@ -140,21 +212,11 @@ impl rpc_api::node::RpcServer .unwrap() } - async fn get_block( + async fn get_block_hash( &self, - block_hash: thunder::types::BlockHash, - ) -> RpcResult> { - let Some(header) = self - .app - .node - .try_get_header(block_hash) - .map_err(custom_err)? - else { - return Ok(None); - }; - let body = self.app.node.get_body(block_hash).map_err(custom_err)?; - let block = thunder::types::Block { header, body }; - Ok(Some(block)) + height: u32, + ) -> RpcResult> { + self.app.node.try_get_block_hash(height).map_err(custom_err) } async fn get_best_sidechain_block_hash( @@ -235,6 +297,25 @@ impl rpc_api::node::RpcServer Ok(res) } + async fn get_withdrawal_bundle( + &self, + m6id: M6id, + ) -> RpcResult> { + let Some((bundle_info, bundle_status)) = self + .app + .node + .try_get_withdrawal_bundle(&m6id) + .map_err(custom_err)? + else { + return Ok(None); + }; + let response = rpc_api::node::GetWithdrawalBundleResponse { + info: bundle_info, + status: bundle_status, + }; + Ok(Some(response)) + } + async fn getblockcount(&self) -> RpcResult { let height = self.app.node.try_get_height().map_err(custom_err)?; let block_count = height.map_or(0, |height| height + 1); @@ -569,6 +650,11 @@ pub async fn run_server( let rpc_server_impl = RpcServerImpl:: { app: app.clone() }; let mut rpc_module = rpc_api::open_api::RpcServer::into_rpc(rpc_server_impl.clone()); + rpc_module.merge( + rpc_api::node::get_block::untyped::RpcServer::into_rpc( + rpc_server_impl.clone(), + ), + )?; rpc_module .merge(rpc_api::node::RpcServer::into_rpc(rpc_server_impl))?; server.start(rpc_module) @@ -578,6 +664,11 @@ pub async fn run_server( let mut rpc_module = rpc_api::open_api::RpcServer::into_rpc( PrivateOnlyRpcServerImpl, ); + rpc_module.merge( + rpc_api::node::get_block::untyped::RpcServer::into_rpc( + rpc_server_impl.clone(), + ), + )?; rpc_module.merge(rpc_api::node::PrivateRpcServer::into_rpc( rpc_server_impl.clone(), ))?; @@ -603,6 +694,11 @@ pub async fn run_server( rpc_module.merge(rpc_api::node::PrivateRpcServer::into_rpc( rpc_server_impl.clone(), ))?; + rpc_module.merge( + rpc_api::node::get_block::untyped::RpcServer::into_rpc( + rpc_server_impl.clone(), + ), + )?; rpc_module.merge(rpc_api::node::RpcServer::into_rpc( rpc_server_impl.clone(), ))?; diff --git a/cli/lib.rs b/cli/lib.rs index d324a872..50d81512 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -4,9 +4,10 @@ use clap::{Parser, Subcommand}; use http::HeaderMap; use jsonrpsee::{core::client::ClientT, http_client::HttpClientBuilder}; -use thunder::types::{Address, Txid, net::PeerAddress}; +use thunder::types::{Address, M6id, Txid, net::PeerAddress}; use thunder_app_rpc_api::{ - node::{PrivateRpcClient as _, RpcClient as _}, + node::{PrivateRpcClient as _, RpcClient as _, get_block::RpcClient as _}, + typewit::const_marker::Bool, wallet::RpcClient as _, }; use tracing_subscriber::layer::SubscriberExt as _; @@ -79,7 +80,13 @@ pub enum Command { /// Get the block with specified block hash, if it exists GetBlock { block_hash: thunder::types::BlockHash, + verbose: Option, }, + /// Get the current block count + GetBlockcount, + /// Get the block hash at the specified height in the active chain, if it + /// exists + GetBlockHash { height: u32 }, /// Assemble a block to blind merge mine, without requesting BMM for it GetBlockTemplate, /// Get mainchain blocks that commit to a specified block hash @@ -104,8 +111,8 @@ pub enum Command { GetWalletAddresses, /// Get wallet UTXOs GetWalletUtxos, - /// Get the current block count - GetBlockcount, + /// Get withdrawal bundle by M6id + GetWithdrawalBundle { m6id: M6id }, /// Invalidate a block, potentially re-orging to a valid ancestor of the /// current tip. InvalidateBlock { @@ -190,6 +197,10 @@ where rpc_client.connect_block(block, main_block_hash).await?; format!("{accepted}") } + Command::GetBlockHash { height } => { + let block_hash = rpc_client.get_block_hash(height).await?; + serde_json::to_string_pretty(&block_hash)? + } Command::ConnectPeer { addr } => { let () = rpc_client.connect_peer(addr).await?; String::default() @@ -237,10 +248,6 @@ where rpc_client.forget_peer(addr).await?; String::default() } - Command::GetBlock { block_hash } => { - let block = rpc_client.get_block(block_hash).await?; - serde_json::to_string_pretty(&block)? - } Command::GetBestMainchainBlockHash => { let block_hash = rpc_client.get_best_mainchain_block_hash().await?; serde_json::to_string_pretty(&block_hash)? @@ -249,6 +256,25 @@ where let block_hash = rpc_client.get_best_sidechain_block_hash().await?; serde_json::to_string_pretty(&block_hash)? } + Command::GetBlock { + block_hash, + verbose, + } => match verbose { + Some(true) => { + let block = + rpc_client.get_block(block_hash, Bool::).await?; + serde_json::to_string_pretty(&block)? + } + Some(false) | None => { + let block = + rpc_client.get_block(block_hash, Bool::).await?; + serde_json::to_string_pretty(&block)? + } + }, + Command::GetBlockcount => { + let blockcount = rpc_client.getblockcount().await?; + format!("{blockcount}") + } Command::GetBlockTemplate => { let template = rpc_client.get_block_template().await?; serde_json::to_string_pretty(&template)? @@ -285,9 +311,10 @@ where let utxos = rpc_client.get_wallet_utxos().await?; serde_json::to_string_pretty(&utxos)? } - Command::GetBlockcount => { - let blockcount = rpc_client.getblockcount().await?; - format!("{blockcount}") + Command::GetWithdrawalBundle { m6id } => { + let withdrawal_bundle = + rpc_client.get_withdrawal_bundle(m6id).await?; + serde_json::to_string_pretty(&withdrawal_bundle)? } Command::InvalidateBlock { block_hash } => { let () = rpc_client.invalidate_block(block_hash).await?; diff --git a/lib/node/mod.rs b/lib/node/mod.rs index 2f1a8929..5d058c5b 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -10,6 +10,9 @@ use bitcoin::amount::CheckedSum; use fallible_iterator::{FallibleIterator, IteratorExt}; use futures::Stream; use sneed::{DbError, Env, EnvError, RwTxnError}; +use thunder_types::{ + M6id, WithdrawalBundleStatus, state::WithdrawalBundleInfo, +}; use tokio::sync::Mutex; use tonic::transport::Channel; @@ -17,7 +20,7 @@ use crate::{ archive::Archive, mempool::{self, MemPool}, net::{DialKnownPeersHandle, Net}, - state::State, + state::{self, State}, types::{ Accumulator, Address, AmountOverflowError, AmountUnderflowError, Authorized, AuthorizedTransaction, BlockHash, BmmResult, Body, @@ -380,7 +383,7 @@ where Ok(self.archive.get_header(&txn, block_hash)?) } - /// Get the block hash at the specified height in the current chain, + /// Get the block hash at the specified height in the active chain, /// if it exists pub fn try_get_block_hash( &self, @@ -558,6 +561,19 @@ where } } + pub fn try_get_withdrawal_bundle( + &self, + m6id: &M6id, + ) -> Result, Error> + { + let rotxn = self.env.read_txn()?; + let res = self + .state + .try_get_withdrawal_bundle(&rotxn, m6id) + .map_err(state::Error::from)?; + Ok(res) + } + pub fn try_get_pending_withdrawal_bundle( &self, ) -> Result, Error> { diff --git a/lib/state/mod.rs b/lib/state/mod.rs index b087e011..922cb495 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -1,10 +1,9 @@ //! Sidechain state as of the current sidechain tip -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; use fallible_iterator::FallibleIterator as _; use heed::types::SerdeBincode; -use serde::{Deserialize, Serialize}; use sneed::{ DatabaseUnique, RoTxn, RwTxn, UnitKey, db::error::{self as db_error, Error as DbError}, @@ -21,6 +20,7 @@ use crate::{ PointedOutputRef, SpentOutput, Transaction, UtreexoNodeHash, UtreexoProof, VERSION, Verify, Version, WithdrawalBundle, WithdrawalBundleStatus, proto::mainchain::TwoWayPegData, + state::WithdrawalBundleInfo, }, util::Watchable, }; @@ -48,20 +48,6 @@ pub struct PrevalidatedBlock { pub accumulator_diff: crate::types::AccumulatorDiff, } -/// Information we have regarding a withdrawal bundle -#[derive(Debug, Deserialize, Serialize)] -enum WithdrawalBundleInfo { - /// Withdrawal bundle is known - Known(WithdrawalBundle), - /// Withdrawal bundle is unknown but unconfirmed / failed - Unknown, - /// If an unknown withdrawal bundle is confirmed, ALL UTXOs are - /// considered spent. - UnknownConfirmed { - spend_utxos: BTreeMap, - }, -} - #[derive(Clone)] pub struct State { /// Current tip @@ -253,6 +239,22 @@ impl State { Ok(Some((failed_height, latest_failed_m6id))) } + pub fn try_get_withdrawal_bundle( + &self, + rotxn: &RoTxn, + m6id: &M6id, + ) -> Result< + Option<(WithdrawalBundleInfo, WithdrawalBundleStatus)>, + db_error::TryGet, + > { + let Some((bundle_info, bundle_status)) = + self.withdrawal_bundles.try_get(rotxn, m6id)? + else { + return Ok(None); + }; + Ok(Some((bundle_info, bundle_status.latest().value))) + } + /// Get the current Utreexo accumulator pub fn get_accumulator(&self, rotxn: &RoTxn) -> Result { let accumulator = self diff --git a/rpc-api/Cargo.toml b/rpc-api/Cargo.toml index 340e5272..f0e934fb 100644 --- a/rpc-api/Cargo.toml +++ b/rpc-api/Cargo.toml @@ -8,13 +8,18 @@ version.workspace = true [dependencies] bitcoin = { workspace = true, features = ["serde"] } +const-hex = { workspace = true, features = ["serde"] } jsonrpsee = { workspace = true, features = ["client", "macros", "server"] } l2l-openapi = { workspace = true } serde = {workspace = true } serde_json = { workspace = true } thunder_types = { path = "../types" } +typewit = { workspace = true, features = ["serde"] } utoipa = { workspace = true } +[dev-dependencies] +anyhow = { workspace = true } + [lints] workspace = true diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index 054a52be..4f565dc0 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -1,5 +1,8 @@ //! RPC API +/// Exported for convenience +pub use typewit; + mod schema; pub mod open_api { @@ -25,25 +28,22 @@ pub mod node { use l2l_openapi::open_api; use serde::{Deserialize, Serialize}; use thunder_types::{ - Address, Authorized, Block, BlockHash, MerkleRoot, OutPoint, Output, - OutputContent, Pointed, PointedOutput, SpentOutput, Transaction, Txid, - WithdrawalBundle, - net::{Peer, PeerAddress}, - schema as thunder_schema, + Address, Authorization, Authorized, Block, BlockHash, Body, Header, + InPoint, M6id, MerkleRoot, OutPoint, Output, OutputContent, Pointed, + PointedOutput, SpentOutput, Transaction, Txid, WithdrawalBundle, + WithdrawalBundleStatus, + net::{Peer, PeerAddress, PeerConnectionStatus}, + state::WithdrawalBundleInfo, }; + use typewit::const_marker::Bool; use utoipa::ToSchema; use crate::{open_api, schema}; - #[open_api(ref_schemas[ - Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, - schema::BitcoinTxid, thunder_schema::BitcoinAddr, - thunder_schema::BitcoinOutPoint, - ])] + #[open_api] #[rpc(client, server, server_bounds(Self: open_api::RpcServer))] pub trait PrivateRpc { /// Connect to a peer - #[open_api_method(output_schema(ToSchema))] #[method(name = "connect_peer")] async fn connect_peer(&self, addr: PeerAddress) -> RpcResult<()>; @@ -61,7 +61,6 @@ pub mod node { ) -> RpcResult<()>; /// Remove a tx from the mempool - #[open_api_method(output_schema(ToSchema))] #[method(name = "remove_from_mempool")] async fn remove_from_mempool(&self, txid: Txid) -> RpcResult<()>; @@ -70,6 +69,181 @@ pub mod node { async fn stop(&self); } + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] + pub struct TransactionVerbose { + #[serde(flatten)] + pub tx: Transaction, + #[serde(with = "const_hex")] + pub canonical_bytes: Vec, + } + + pub mod get_block { + use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + use serde::{Deserialize, Serialize, de::DeserializeOwned}; + use thunder_types::{Authorization, Header, Output}; + use utoipa::ToSchema; + + use crate::node::TransactionVerbose; + + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] + pub struct BodyVerbose { + pub coinbase: Vec, + pub transactions: Vec, + pub authorizations: Vec, + } + + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] + pub struct BlockVerbose { + pub header: Header, + pub body: BodyVerbose, + } + + pub mod verbosity { + use serde::{Serialize, de::DeserializeOwned}; + use thunder_types::Block; + use typewit::const_marker::Bool; + + use crate::node::get_block::BlockVerbose; + + mod private { + pub trait Sealed {} + } + + pub trait Verbosity: Serialize + private::Sealed { + type Response: DeserializeOwned + Serialize; + } + + impl private::Sealed for Bool {} + + impl Verbosity for Bool { + type Response = BlockVerbose; + } + + impl Verbosity for Bool { + type Response = Block; + } + + impl private::Sealed for Option> {} + + impl Verbosity for Option> { + type Response = as Verbosity>::Response; + } + } + pub use verbosity::Verbosity; + + #[rpc(client, server, server_bounds( + V: DeserializeOwned + Verbosity, + ::Response: Clone + 'static, + ))] + pub trait Rpc + where + V: Verbosity, + { + /// Get the block with specified block hash, if it exists + #[method(name = "get_block")] + async fn get_block( + &self, + block_hash: thunder_types::BlockHash, + verbose: V, + ) -> RpcResult>; + } + + pub mod untyped { + use jsonrpsee::{ + core::{RpcResult, async_trait}, + proc_macros::rpc, + }; + use l2l_openapi::open_api; + use serde::Serialize; + use thunder_types::{ + Address, Authorization, Block, BlockHash, Body, Header, + MerkleRoot, Output, OutputContent, Transaction, Txid, + }; + use typewit::const_marker::Bool; + use utoipa::ToSchema; + + use crate::{ + node::{ + TransactionVerbose, + get_block::{ + BlockVerbose, BodyVerbose, RpcServer as GetBlock, + }, + }, + schema, + }; + + mod private { + pub trait Sealed {} + } + + impl private::Sealed for S where + S: GetBlock> + GetBlock> + { + } + + #[derive(Clone, Serialize, ToSchema)] + #[serde(untagged)] + pub enum Response { + NonVerbose(Block), + Verbose(BlockVerbose), + } + + /// This trait exists only as a bound, and should not be implemented + /// manually + #[open_api(ref_schemas[ + Address, Authorization, Block, BlockHash, BlockVerbose, Body, + BodyVerbose, Header, MerkleRoot, Output, OutputContent, + Transaction, TransactionVerbose, Txid, schema::BitcoinAddr, + schema::BitcoinBlockHash, schema::BitcoinOutPoint, + schema::UtreexoNodeHash, schema::UtreexoProof, + ])] + #[rpc(server, server_bounds(Self: private::Sealed))] + pub trait Rpc { + /// Get the block with specified block hash, if it exists + #[method(name = "get_block")] + async fn get_block( + &self, + block_hash: thunder_types::BlockHash, + verbose: Option, + ) -> RpcResult>; + } + + #[async_trait] + impl RpcServer for S + where + S: GetBlock> + GetBlock>, + { + async fn get_block( + &self, + block_hash: thunder_types::BlockHash, + verbose: Option, + ) -> RpcResult> { + match verbose { + Some(true) => { + >>::get_block( + self, + block_hash, + Bool::, + ) + .await + .map(|res| res.map(Response::Verbose)) + } + Some(false) | None => { + >>::get_block( + self, + block_hash, + Bool::, + ) + .await + .map(|res| res.map(Response::NonVerbose)) + } + } + } + } + } + pub use untyped::RpcDoc; + } + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct GetTransactionResponse { pub tx: Transaction, @@ -77,12 +251,36 @@ pub mod node { pub block_hash: Option, } - #[open_api(ref_schemas[ - Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, - schema::BitcoinTxid, thunder_schema::BitcoinAddr, - thunder_schema::BitcoinOutPoint, - ])] - #[rpc(client, server, server_bounds(Self: open_api::RpcServer))] + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] + pub struct GetWithdrawalBundleResponse { + pub info: WithdrawalBundleInfo, + pub status: WithdrawalBundleStatus, + } + + #[open_api( + merge_apis[get_block::RpcDoc], + ref_schemas[ + Address, Authorization, BlockHash, Body, Header, InPoint, M6id, + MerkleRoot, OutPoint, Output, OutputContent, PeerConnectionStatus, + SpentOutput, Transaction, Txid, WithdrawalBundle, + WithdrawalBundleInfo, WithdrawalBundleStatus, schema::BitcoinAddr, + schema::BitcoinBlockHash, schema::BitcoinOutPoint, + schema::BitcoinTransaction, schema::SocketAddr, + schema::UtreexoNodeHash, schema::UtreexoProof, + ], + )] + #[rpc( + client, + client_bounds( + Self: + get_block::RpcClient> + + get_block::RpcClient> + ), + server, + server_bounds( + Self: open_api::RpcServer + get_block::untyped::RpcServer, + ), + )] pub trait Rpc { /// Connect a block template for which a BMM request was included in the /// specified mainchain block. Returns `true` if it was accepted as the new @@ -93,21 +291,25 @@ pub mod node { &self, block: Block, #[open_api_method_arg(schema( - PartialSchema = "thunder_schema::BitcoinBlockHash" + PartialSchema = "schema::BitcoinBlockHash" ))] main_block_hash: bitcoin::BlockHash, ) -> RpcResult; - /// Get the block with specified block hash, if it exists - #[method(name = "get_block")] - async fn get_block( + /// Get the block hash at the specified height in the active chain, + /// if it exists + #[open_api_method(output_schema( + PartialSchema = "schema::Optional" + ))] + #[method(name = "get_block_hash")] + async fn get_block_hash( &self, - block_hash: thunder_types::BlockHash, - ) -> RpcResult>; + height: u32, + ) -> RpcResult>; /// Get mainchain blocks that commit to a specified block hash #[open_api_method(output_schema( - PartialSchema = "thunder_schema::BitcoinBlockHash" + PartialSchema = "schema::BitcoinBlockHash" ))] #[method(name = "get_bmm_inclusions")] async fn get_bmm_inclusions( @@ -117,7 +319,7 @@ pub mod node { /// Get the best mainchain block hash known by Thunder #[open_api_method(output_schema( - PartialSchema = "schema::Optional" + PartialSchema = "schema::Optional" ))] #[method(name = "get_best_mainchain_block_hash")] async fn get_best_mainchain_block_hash( @@ -154,6 +356,13 @@ pub mod node { addresses: HashSet
, ) -> RpcResult>; + /// Get withdrawal bundle by M6id + #[method(name = "get_withdrawal_bundle")] + async fn get_withdrawal_bundle( + &self, + m6id: M6id, + ) -> RpcResult>; + /// Get the current block count #[method(name = "getblockcount")] async fn getblockcount(&self) -> RpcResult; @@ -197,9 +406,9 @@ pub mod wallet { use l2l_openapi::open_api; use serde::{Deserialize, Serialize}; use thunder_types::{ - Address, Authorized, Block, BlockHash, MerkleRoot, OutPoint, Output, - OutputContent, PointedOutput, Transaction, Txid, - schema as thunder_schema, wallet::Balance, + Address, Authorization, Authorized, Block, BlockHash, Body, Header, + MerkleRoot, OutPoint, Output, OutputContent, PointedOutput, + Transaction, Txid, wallet::Balance, }; use utoipa::ToSchema; @@ -217,9 +426,10 @@ pub mod wallet { } #[open_api(ref_schemas[ - Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, - schema::BitcoinTxid, thunder_schema::BitcoinAddr, - thunder_schema::BitcoinOutPoint, + Address, Authorization, Block, BlockHash, Body, Header, MerkleRoot, + OutPoint, Output, OutputContent, Transaction, Txid, + schema::BitcoinAddr, schema::BitcoinBlockHash, schema::BitcoinOutPoint, + schema::UtreexoNodeHash, schema::UtreexoProof, ])] #[rpc(client, server, server_bounds(Self: open_api::RpcServer))] pub trait Rpc { @@ -255,7 +465,7 @@ pub mod wallet { async fn create_withdrawal( &self, #[open_api_method_arg(schema( - PartialSchema = "thunder_schema::BitcoinAddr" + PartialSchema = "schema::BitcoinAddr" ))] mainchain_address: bitcoin::Address< bitcoin::address::NetworkUnchecked, @@ -319,3 +529,6 @@ pub mod wallet { ) -> RpcResult>; } } + +#[cfg(test)] +mod test; diff --git a/rpc-api/schema.rs b/rpc-api/schema.rs index f503a54f..9821e964 100644 --- a/rpc-api/schema.rs +++ b/rpc-api/schema.rs @@ -7,6 +7,8 @@ use utoipa::{ openapi::{self, RefOr, Schema}, }; +pub use thunder_types::schema::*; + pub struct BitcoinTxid; impl PartialSchema for BitcoinTxid { diff --git a/rpc-api/test.rs b/rpc-api/test.rs new file mode 100644 index 00000000..29489558 --- /dev/null +++ b/rpc-api/test.rs @@ -0,0 +1,320 @@ +use std::collections::BTreeSet; + +use utoipa::openapi::{self, Ref, RefOr, Schema}; + +/// Get all component refs +trait ComponentRefs { + fn component_refs(&self) -> impl Iterator + '_; +} + +impl ComponentRefs for Box +where + T: ComponentRefs, +{ + fn component_refs(&self) -> impl Iterator + '_ { + ::component_refs(self) + } +} + +impl ComponentRefs for Vec +where + T: ComponentRefs, +{ + fn component_refs(&self) -> impl Iterator + '_ { + self.iter().flat_map(|item| item.component_refs()) + } +} + +impl ComponentRefs for Option +where + T: ComponentRefs, +{ + fn component_refs(&self) -> impl Iterator + '_ { + self.iter().flat_map(|item| item.component_refs()) + } +} + +impl ComponentRefs for Ref { + fn component_refs(&self) -> impl Iterator + '_ { + std::iter::once(self) + } +} + +impl ComponentRefs for RefOr +where + T: ComponentRefs, +{ + fn component_refs(&self) -> impl Iterator + '_ { + match self { + RefOr::Ref(r) => Box::new(r.component_refs()) + as Box + '_>, + RefOr::T(t) => Box::new(t.component_refs()), + } + } +} + +impl ComponentRefs for openapi::AllOf { + fn component_refs(&self) -> impl Iterator + '_ { + self.items.component_refs() + } +} + +impl ComponentRefs for openapi::schema::AnyOf { + fn component_refs(&self) -> impl Iterator + '_ { + self.items.component_refs() + } +} + +impl ComponentRefs for openapi::schema::ArrayItems { + fn component_refs(&self) -> impl Iterator + '_ { + (match self { + Self::False => None, + Self::RefOrSchema(roschema) => Some(roschema.component_refs()), + }) + .into_iter() + .flatten() + } +} + +impl ComponentRefs for openapi::Array { + fn component_refs(&self) -> impl Iterator + '_ { + let items_refs = self.items.component_refs(); + let prefix_items_refs = self.prefix_items.component_refs(); + items_refs.chain(prefix_items_refs) + } +} + +impl ComponentRefs for openapi::schema::AdditionalProperties +where + T: ComponentRefs, +{ + fn component_refs(&self) -> impl Iterator + '_ { + (match self { + Self::RefOr(ref_or) => Some(ref_or.component_refs()), + Self::FreeForm(_) => None, + }) + .into_iter() + .flatten() + } +} + +impl ComponentRefs for openapi::Object { + fn component_refs(&self) -> impl Iterator + '_ { + let properties_refs = self + .properties + .values() + .flat_map(|ref_or_schema| ref_or_schema.component_refs()); + let additional_properties_refs = + self.additional_properties.component_refs(); + let property_names = self.property_names.component_refs(); + properties_refs + .chain(additional_properties_refs) + .chain(property_names) + } +} + +impl ComponentRefs for openapi::OneOf { + fn component_refs(&self) -> impl Iterator + '_ { + self.items.component_refs() + } +} + +impl ComponentRefs for Schema { + fn component_refs(&self) -> impl Iterator + '_ { + match self { + Schema::AllOf(all_of) => Box::new(all_of.component_refs()) + as Box + '_>, + Schema::AnyOf(any_of) => Box::new(any_of.component_refs()), + Schema::Array(array) => Box::new(array.component_refs()), + Schema::Object(object) => Box::new(object.component_refs()), + Schema::OneOf(oneof) => Box::new(oneof.component_refs()), + _ => Box::new(std::iter::empty()), + } + } +} + +impl ComponentRefs for openapi::example::Example { + fn component_refs(&self) -> impl Iterator + '_ { + std::iter::empty() + } +} + +impl ComponentRefs for openapi::path::Parameter { + fn component_refs(&self) -> impl Iterator + '_ { + self.schema.component_refs() + } +} + +impl ComponentRefs for openapi::Header { + fn component_refs(&self) -> impl Iterator + '_ { + self.schema.component_refs() + } +} + +impl ComponentRefs for openapi::encoding::Encoding { + fn component_refs(&self) -> impl Iterator + '_ { + self.headers + .values() + .flat_map(|header| header.component_refs()) + } +} + +impl ComponentRefs for openapi::Content { + fn component_refs(&self) -> impl Iterator + '_ { + let schema_ref = self.schema.component_refs(); + let example_refs = self + .examples + .values() + .flat_map(|ref_or_example| ref_or_example.component_refs()); + let encoding_refs = self + .encoding + .values() + .flat_map(|encoding| encoding.component_refs()); + schema_ref.chain(example_refs).chain(encoding_refs) + } +} + +impl ComponentRefs for openapi::request_body::RequestBody { + fn component_refs(&self) -> impl Iterator + '_ { + self.content + .values() + .flat_map(|content| content.component_refs()) + } +} + +impl ComponentRefs for openapi::link::Link { + fn component_refs(&self) -> impl Iterator + '_ { + std::iter::empty() + } +} + +impl ComponentRefs for openapi::Response { + fn component_refs(&self) -> impl Iterator + '_ { + let header_refs = self + .headers + .values() + .flat_map(|header| header.component_refs()); + let content_refs = self + .content + .values() + .flat_map(|content| content.component_refs()); + let link_refs = self + .links + .values() + .flat_map(|ref_or_link| ref_or_link.component_refs()); + header_refs.chain(content_refs).chain(link_refs) + } +} + +impl ComponentRefs for openapi::Responses { + fn component_refs(&self) -> impl Iterator + '_ { + self.responses + .values() + .flat_map(|ref_or_response| ref_or_response.component_refs()) + } +} + +impl ComponentRefs for openapi::path::Operation { + fn component_refs(&self) -> impl Iterator + '_ { + let param_refs = self.parameters.component_refs(); + let request_body_refs = self.request_body.component_refs(); + let responses_refs = self.responses.component_refs(); + param_refs.chain(request_body_refs).chain(responses_refs) + } +} + +impl ComponentRefs for openapi::PathItem { + fn component_refs(&self) -> impl Iterator + '_ { + let param_refs = self.parameters.component_refs(); + let operation_refs = { + let operations = [ + &self.get, + &self.put, + &self.post, + &self.delete, + &self.options, + &self.head, + &self.patch, + &self.trace, + ]; + operations + .into_iter() + .flat_map(|operation| operation.component_refs()) + }; + param_refs.chain(operation_refs) + } +} + +impl ComponentRefs for openapi::Paths { + fn component_refs(&self) -> impl Iterator + '_ { + self.paths + .values() + .flat_map(|path_item| path_item.component_refs()) + } +} + +impl ComponentRefs for openapi::Components { + fn component_refs(&self) -> impl Iterator + '_ { + let schemas_refs = self + .schemas + .values() + .flat_map(|ref_or_schema| ref_or_schema.component_refs()); + let response_refs = self + .responses + .values() + .flat_map(|ref_or_response| ref_or_response.component_refs()); + schemas_refs.chain(response_refs) + } +} + +impl ComponentRefs for openapi::OpenApi { + fn component_refs(&self) -> impl Iterator + '_ { + let paths_refs = self.paths.component_refs(); + let component_refs = self.components.component_refs(); + paths_refs.chain(component_refs) + } +} + +// Check for errors within a schema. +// This is a WIP and may not cover all possible errors. +fn check_schema() -> anyhow::Result<()> +where + T: utoipa::OpenApi, +{ + let schema: openapi::OpenApi = ::openapi(); + let component_ref_locations = BTreeSet::<&str>::from_iter( + schema + .component_refs() + .map(|r#ref| r#ref.ref_location.as_str()), + ); + let component_schemas = + BTreeSet::<&str>::from_iter(schema.components.iter().flat_map( + |components| components.schemas.keys().map(|s| s.as_str()), + )); + // TODO: check that there are no ref cycles here + for ref_loc in &component_ref_locations { + let Some(loc) = ref_loc.strip_prefix("#/components/schemas/") else { + anyhow::bail!("Unexpected prefix in ref location: `{ref_loc}`"); + }; + if !component_schemas.contains(loc) { + anyhow::bail!("Missing schema referenced as `{ref_loc}`") + } + } + // Check for redundant components + for component in component_schemas { + let component_ref = format!("#/components/schemas/{component}"); + if !component_ref_locations.contains(component_ref.as_str()) { + anyhow::bail!("No references to {component_ref}") + } + } + Ok(()) +} + +#[test] +fn check_schemas() -> anyhow::Result<()> { + let () = check_schema::()?; + let () = check_schema::()?; + let () = check_schema::()?; + let () = check_schema::()?; + Ok(()) +} diff --git a/types/hashes.rs b/types/hashes.rs index b9d36046..e7f06c73 100644 --- a/types/hashes.rs +++ b/types/hashes.rs @@ -5,7 +5,6 @@ use blake3::Hasher; use borsh::{BorshDeserialize, BorshSerialize}; use const_hex::FromHex; use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; use crate::util::serde::hexstr_human_readable; @@ -217,11 +216,8 @@ impl utoipa::ToSchema for Txid { } } -#[derive( - Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, ToSchema, -)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[repr(transparent)] -#[schema(value_type = crate::schema::BitcoinOutPoint)] #[serde(transparent)] pub struct M6id(pub bitcoin::Txid); @@ -232,6 +228,28 @@ impl std::fmt::Display for M6id { } } +impl FromStr for M6id { + type Err = ::Err; + fn from_str(s: &str) -> Result { + let inner = bitcoin::Txid::from_str(s)?; + Ok(Self(inner)) + } +} + +impl utoipa::PartialSchema for M6id { + fn schema() -> utoipa::openapi::RefOr { + let obj = + utoipa::openapi::Object::with_type(utoipa::openapi::Type::String); + utoipa::openapi::RefOr::T(utoipa::openapi::Schema::Object(obj)) + } +} + +impl utoipa::ToSchema for M6id { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("M6id") + } +} + /// A block hash that is known to be non-zero. Bitcoin core often uses the /// all-zeros block hash to represent `Option::::None`. #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] diff --git a/types/lib.rs b/types/lib.rs index 37535fbc..e6f05fb4 100644 --- a/types/lib.rs +++ b/types/lib.rs @@ -30,6 +30,7 @@ pub use hashes::{ }; pub mod net; pub mod schema; +pub mod state; pub mod transaction; pub use transaction::{ Authorized, AuthorizedTransaction, Content as OutputContent, @@ -79,7 +80,9 @@ pub enum WithdrawalBundleEventStatus { Submitted, } -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive( + Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema, +)] pub enum WithdrawalBundleStatus { Confirmed, /// Formerly pending bundle diff --git a/types/state.rs b/types/state.rs new file mode 100644 index 00000000..dfd0effe --- /dev/null +++ b/types/state.rs @@ -0,0 +1,20 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::{OutPoint, Output, WithdrawalBundle}; + +/// Information we have regarding a withdrawal bundle +#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] +pub enum WithdrawalBundleInfo { + /// Withdrawal bundle is known + Known(WithdrawalBundle), + /// Withdrawal bundle is unknown but unconfirmed / failed + Unknown, + /// If an unknown withdrawal bundle is confirmed, ALL UTXOs are + /// considered spent. + UnknownConfirmed { + spend_utxos: BTreeMap, + }, +} diff --git a/types/transaction.rs b/types/transaction.rs index 7af8380c..70d88bad 100644 --- a/types/transaction.rs +++ b/types/transaction.rs @@ -400,10 +400,6 @@ mod content { pub fn is_withdrawal(&self) -> bool { matches!(self, Self::Withdrawal { .. }) } - - pub(crate) fn schema_ref() -> utoipa::openapi::Ref { - utoipa::openapi::Ref::new("OutputContent") - } } impl GetValue for Content { @@ -539,7 +535,6 @@ pub use content::Content; )] pub struct Output { pub address: Address, - #[schema(schema_with = Content::schema_ref)] pub content: Content, } @@ -604,6 +599,12 @@ impl Transaction { hash_with_scratch_buffer(self).into() } + /// Canonical encoding as bytes. The canonical encoding is used for hashing, + /// but other encodings may be used at eg. networking, rpc levels. + pub fn canonical_bytes(&self) -> borsh::io::Result> { + borsh::to_vec(&self) + } + /// Canonical size in bytes. The canonical encoding is used for hashing, /// but other encodings may be used at eg. networking, rpc levels. pub fn canonical_size(&self) -> u64 {