Skip to content

Commit

Permalink
feature: allow jsonrpc finding utxos
Browse files Browse the repository at this point in the history
This commit update the gettxout (and removes findtxout) from our json
rpc that now uses compact block filters to find any given utxo. We also
use the same filters to figure if the TXO is spent or not.
  • Loading branch information
Davidson-Souza committed Dec 1, 2023
1 parent 6533075 commit 157f7b0
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 71 deletions.
18 changes: 5 additions & 13 deletions crates/floresta-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ fn get_req(cmd: &Cli) -> (Vec<Box<RawValue>>, String) {
Methods::GetRoots => "getroots",
Methods::GetBlock { .. } => "getblock",
Methods::GetPeerInfo => "getpeerinfo",
Methods::FindUtxo { .. } => "findtxout",
Methods::ListTransactions => "gettransactions",
Methods::Stop => "stop",
Methods::AddNode { .. } => "addnode",
Expand Down Expand Up @@ -86,9 +85,6 @@ fn get_req(cmd: &Cli) -> (Vec<Box<RawValue>>, String) {
Methods::GetBlock { hash } => vec![arg(hash)],
Methods::GetPeerInfo => Vec::new(),
Methods::ListTransactions => Vec::new(),
Methods::FindUtxo { height, txid, vout } => {
vec![arg(height), arg(txid), arg(vout)]
}
Methods::Stop => Vec::new(),
Methods::AddNode { node } => {
vec![arg(node)]
Expand Down Expand Up @@ -137,10 +133,6 @@ pub enum Methods {
/// Returns the hash of the block associated with height
#[command(name = "getblockhash")]
GetBlockHash { height: u32 },
/// Returns information about a transaction output, assuming it is cached by our watch
/// only wallet
#[command(name = "gettxout")]
GetTxOut { txid: Txid, vout: u32 },
/// Returns the proof that one or more transactions were included in a block
#[command(name = "gettxproof")]
GetTxProof {
Expand Down Expand Up @@ -174,11 +166,11 @@ pub enum Methods {
/// List all transactions we are watching
#[command(name = "listtransactions")]
ListTransactions,
/// Finds a TXO by its outpoint and block height. Since we don't have a UTXO set
/// the block height is required to find the UTXO. Note that this command doesn't
/// check if the UTXO is spent or not.
#[command(name = "findutxo")]
FindUtxo { height: u32, txid: Txid, vout: u32 },
/// Returns the value associated with a UTXO, if it's still not spent.
/// This function only works properly if we have the compact block filters
/// feature enabled
#[command(name = "gettxout")]
GetTxOut { txid: Txid, vout: u32 },
/// Stops the node
#[command(name = "stop")]
Stop,
Expand Down
17 changes: 9 additions & 8 deletions crates/floresta-compact-filters/src/kv_filter_database.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use std::path::PathBuf;

use bitcoin::util::bip158::BlockFilter;
use kv::{Bucket, Integer, Config};
use kv::{Bucket, Config, Integer};

use crate::BlockFilterStore;

/// Stores the block filters insinde a kv database
pub struct KvFilterStore<'a> {
bucket: Bucket<'a, Integer, Vec<u8>>,
#[derive(Clone)]
pub struct KvFilterStore {
bucket: Bucket<'static, Integer, Vec<u8>>,
}

impl KvFilterStore<'_> {
impl KvFilterStore {
/// Creates a new [KvFilterStore] that stores it's content in `datadir`.
///
/// If the path does't exist it'll be created. This store uses compression by default, if you
Expand All @@ -20,15 +21,15 @@ impl KvFilterStore<'_> {
let store = kv::Store::new(kv::Config {
path: datadir.to_owned(),
temporary: false,
use_compression: true,
use_compression: false,
flush_every_ms: None,
cache_capacity: None,
segment_size: None,
}).expect("Could not open store");
})
.expect("Could not open store");

let bucket = store.bucket(Some("cfilters")).unwrap();
KvFilterStore { bucket }

}
/// Creates a new [KvFilterStore] that stores it's content with a given config
pub fn with_config(config: Config) -> Self {
Expand All @@ -38,7 +39,7 @@ impl KvFilterStore<'_> {
}
}

impl BlockFilterStore for KvFilterStore<'_> {
impl BlockFilterStore for KvFilterStore {
fn get_filter(&self, block_height: u64) -> Option<bitcoin::util::bip158::BlockFilter> {
let value = self
.bucket
Expand Down
41 changes: 28 additions & 13 deletions crates/floresta-compact-filters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use log::error;
use std::io::Write;

/// A database that stores our compact filters
pub trait BlockFilterStore {
pub trait BlockFilterStore: Send + Sync {
/// Fetches a block filter
fn get_filter(&self, block_height: u64) -> Option<bip158::BlockFilter>;
/// Stores a new filter
Expand Down Expand Up @@ -140,16 +140,20 @@ impl BlockFilterBackend {
k1: u64::from_le_bytes(k1),
}
}
/// Returns a given filter
pub fn get_filter(&self, block_height: u32) -> Option<bip158::BlockFilter> {
self.storage.get_filter(block_height as u64)
}

/// Build and index a given block height
pub fn filter_block(&self, block: &Block, block_height: u64) -> Result<(), bip158::Error> {
let mut writer = Vec::new();
let mut filter = FilterBuilder::new(&mut writer, FILTER_M, FILTER_P, self.k0, self.k1);

if self.index_inputs {
self.write_inputs(&block.txdata, &mut filter);
}

if self.index_txids {
self.write_txids(&block.txdata, &mut filter);
}
Expand All @@ -158,38 +162,41 @@ impl BlockFilterBackend {
filter.finish()?;

let filter = BlockFilter::new(writer.as_slice());
self.storage.put_filter(block_height, filter);

self.storage.put_filter(block_height, filter);
Ok(())
}

/// Maches a set of filters against out current set of filters
///
/// This function will run over each filter inside the range `[start, end]` and sees
/// if at least one query mathes. It'll return a vector of block heights where it matches.
/// you should download those blocks and see what if there's anything interesting.
pub fn match_any(&self, start: u64, end: u64, query: &[QueryType]) -> Option<Vec<u64>> {
let mut values = query.iter().map(|filter| filter.as_slice());

let mut blocks = Vec::new();
let key = BlockHash::from_inner(self.key);
let values = query
.iter()
.map(|filter| filter.as_slice())
.collect::<Vec<_>>();

for i in start..=end {
if self
.storage
.get_filter(i)?
.match_any(&BlockHash::from_inner(self.key), &mut values)
.ok()?
{
blocks.push(i);
if let Some(result) = self.storage.get_filter(i) {
let result = result.match_any(&key, &mut values.iter().copied()).ok()?;
if result {
blocks.push(i);
}
}
}

Some(blocks)
}

fn write_txids(&self, txs: &Vec<Transaction>, filter: &mut FilterBuilder) {
for tx in txs {
filter.put(tx.txid().as_inner());
}
}

fn write_inputs(&self, txs: &Vec<Transaction>, filter: &mut FilterBuilder) {
for tx in txs {
tx.input.iter().for_each(|input| {
Expand All @@ -200,6 +207,7 @@ impl BlockFilterBackend {
})
}
}

fn write_tx_outs(&self, tx: &Transaction, filter: &mut FilterBuilder) {
for output in tx.output.iter() {
let hash = floresta_common::get_spk_hash(&output.script_pubkey);
Expand All @@ -220,6 +228,7 @@ impl BlockFilterBackend {
}
}
}

fn write_outputs(&self, txs: &Vec<Transaction>, filter: &mut FilterBuilder) {
for tx in txs {
self.write_tx_outs(tx, filter);
Expand Down Expand Up @@ -322,6 +331,7 @@ impl FilterBackendBuilder {
}

/// A serialized output that can be queried against our filter
#[derive(Debug)]
pub struct QueriableOutpoint(pub(crate) [u8; 36]);

impl From<OutPoint> for QueriableOutpoint {
Expand All @@ -334,6 +344,7 @@ impl From<OutPoint> for QueriableOutpoint {
}

/// The type of value we are looking for in a filter.
#[derive(Debug)]
pub enum QueryType {
/// We are looking for a specific outpoint being spent
Input(QueriableOutpoint),
Expand Down Expand Up @@ -365,6 +376,10 @@ pub struct MemoryBlockFilterStorage {
filters: RefCell<Vec<bip158::BlockFilter>>,
}

#[cfg(test)]
#[doc(hidden)]
unsafe impl Sync for MemoryBlockFilterStorage {}

#[doc(hidden)]
#[cfg(test)]
impl BlockFilterStore for MemoryBlockFilterStorage {
Expand Down Expand Up @@ -399,7 +414,7 @@ impl<'a> KvFiltersStore<'a> {
}
}

impl BlockFilterStore for KvFiltersStore<'_> {
impl<'a> BlockFilterStore for KvFiltersStore<'a> {
fn get_filter(&self, block_height: u64) -> Option<bip158::BlockFilter> {
self.bucket
.get(&block_height.into())
Expand Down
4 changes: 3 additions & 1 deletion crates/floresta-wire/src/p2p_wire/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,7 @@ where
// Use this node state to Initial Block download
let mut ibd = UtreexoNode(self.0, IBDNode::default());
try_and_log!(UtreexoNode::<IBDNode, Chain>::run(&mut ibd, kill_signal).await);

// Then take the final state and run the node
self = UtreexoNode(ibd.0, self.1);

Expand All @@ -1107,7 +1108,8 @@ where
.await;

loop {
while let Ok(notification) = timeout(Duration::from_secs(1), self.node_rx.recv()).await
while let Ok(notification) =
timeout(Duration::from_millis(1), self.node_rx.recv()).await
{
try_and_log!(self.handle_notification(notification).await);
}
Expand Down
16 changes: 16 additions & 0 deletions florestad/src/json_rpc/res.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct GetBlockchainInfoRes {
pub progress: f32,
pub difficulty: u64,
}

#[derive(Deserialize, Serialize)]
pub struct RawTxJson {
pub in_active_chain: bool,
Expand All @@ -36,12 +37,14 @@ pub struct RawTxJson {
pub blocktime: u32,
pub time: u32,
}

#[derive(Deserialize, Serialize)]
pub struct TxOutJson {
pub value: u64,
pub n: u32,
pub script_pub_key: ScriptPubKeyJson,
}

#[derive(Deserialize, Serialize)]
pub struct ScriptPubKeyJson {
pub asm: String,
Expand All @@ -51,6 +54,7 @@ pub struct ScriptPubKeyJson {
pub type_: String,
pub address: String,
}

#[derive(Deserialize, Serialize)]
pub struct TxInJson {
pub txid: String,
Expand All @@ -59,11 +63,13 @@ pub struct TxInJson {
pub sequence: u32,
pub witness: Vec<String>,
}

#[derive(Deserialize, Serialize)]
pub struct ScriptSigJson {
pub asm: String,
pub hex: String,
}

#[derive(Deserialize, Serialize)]
pub struct BlockJson {
pub hash: String,
Expand All @@ -85,6 +91,7 @@ pub struct BlockJson {
pub chainwork: String,
pub n_tx: usize,
pub previousblockhash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub nextblockhash: Option<String>,
}

Expand All @@ -96,7 +103,10 @@ pub enum Error {
Chain,
InvalidPort,
InvalidAddress,
Node,
NoBlockFilters,
}

impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Expand All @@ -106,10 +116,13 @@ impl Display for Error {
Error::Chain => "Chain error",
Error::InvalidPort => "Invalid port",
Error::InvalidAddress => "Invalid address",
Error::Node => "Node returned an error",
Error::NoBlockFilters => "You don't have block filters enabled, please start florestad with --cfilters to run this RPC"
};
write!(f, "{}", msg)
}
}

impl From<Error> for i64 {
fn from(val: Error) -> Self {
match val {
Expand All @@ -119,9 +132,12 @@ impl From<Error> for i64 {
Error::InvalidDescriptor => 4,
Error::InvalidPort => 5,
Error::InvalidAddress => 6,
Error::Node => 7,
Error::NoBlockFilters => 8,
}
}
}

impl From<Error> for ErrorCode {
fn from(val: Error) -> Self {
let code = val.into();
Expand Down
Loading

0 comments on commit 157f7b0

Please sign in to comment.