From 4a5fc5e83bea102a7c4628c9ca1765214980aa6c Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 2 May 2018 13:42:13 +0300 Subject: [PATCH] Add very simple Electrum API --- src/daemon.rs | 2 +- src/index.rs | 2 +- src/query.rs | 113 ++++++++++++++++++++++++++++++++++-------------- src/rpc.rs | 96 ++++++++++++++++++++++++++++++++++------ tools/client.py | 39 +++++++++++------ 5 files changed, 189 insertions(+), 63 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 01b0d13..bb8a861 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -17,7 +17,7 @@ pub struct Daemon { url: String, } -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Clone)] pub struct HeaderEntry { height: usize, hash: Sha256dHash, diff --git a/src/index.rs b/src/index.rs index 7510be3..92b0b96 100644 --- a/src/index.rs +++ b/src/index.rs @@ -20,7 +20,7 @@ use types::{Bytes, HeaderMap}; const HASH_LEN: usize = 32; pub const HASH_PREFIX_LEN: usize = 8; -type FullHash = [u8; HASH_LEN]; +pub type FullHash = [u8; HASH_LEN]; pub type HashPrefix = [u8; HASH_PREFIX_LEN]; pub fn hash_prefix(hash: &[u8]) -> HashPrefix { diff --git a/src/query.rs b/src/query.rs index 8506536..1c533a5 100644 --- a/src/query.rs +++ b/src/query.rs @@ -5,7 +5,7 @@ use bitcoin::network::serialize::{deserialize, serialize}; use bitcoin::util::hash::Sha256dHash; use itertools::enumerate; -use daemon::Daemon; +use daemon::{Daemon, HeaderEntry}; use index::{compute_script_hash, hash_prefix, HashPrefix, Index, TxInKey, TxInRow, TxKey, TxOutRow, HASH_PREFIX_LEN}; use store::Store; @@ -17,6 +17,30 @@ pub struct Query<'a> { index: &'a Index, } +pub struct FundingOutput { + pub txn_id: Sha256dHash, + pub height: u32, + pub output_index: usize, + pub value: u64, +} + +pub struct SpendingInput { + pub txn_id: Sha256dHash, + pub height: u32, + pub input_index: usize, +} + +pub struct Status { + pub balance: u64, + pub funding: Vec, + pub spending: Vec, +} + +struct TxnHeight { + txn: Transaction, + height: u32, +} + impl<'a> Query<'a> { pub fn new(store: &'a Store, daemon: &'a Daemon, index: &'a Index) -> Query<'a> { Query { @@ -26,7 +50,7 @@ impl<'a> Query<'a> { } } - fn load_txns(&self, prefixes: Vec) -> Vec { + fn load_txns(&self, prefixes: Vec) -> Vec { let mut txns = Vec::new(); for txid_prefix in prefixes { for row in self.store.scan(&[b"T", &txid_prefix[..]].concat()) { @@ -34,19 +58,20 @@ impl<'a> Query<'a> { let txid: Sha256dHash = deserialize(&key.txid).unwrap(); let txn_bytes = self.daemon.get(&format!("tx/{}.bin", txid.be_hex_string())); let txn: Transaction = deserialize(&txn_bytes).unwrap(); - txns.push(txn) + let height: u32 = bincode::deserialize(&row.value).unwrap(); + txns.push(TxnHeight { txn, height }) } } txns } - fn find_spending_txn(&self, txid: &Sha256dHash, output_index: u32) -> Option { + fn find_spending_input(&self, funding: &FundingOutput) -> Option { let spend_key = bincode::serialize(&TxInKey { code: b'I', - prev_hash_prefix: hash_prefix(&txid[..]), - prev_index: output_index as u16, + prev_hash_prefix: hash_prefix(&funding.txn_id[..]), + prev_index: funding.output_index as u16, }).unwrap(); - let mut spending: Vec = self.load_txns( + let spending_txns: Vec = self.load_txns( self.store .scan(&spend_key) .iter() @@ -57,21 +82,36 @@ impl<'a> Query<'a> { }) .collect(), ); - spending.retain(|item| { - item.input - .iter() - .any(|input| input.prev_hash == *txid && input.prev_index == output_index) - }); - assert!(spending.len() <= 1); - if spending.len() == 1 { - Some(spending.remove(0)) + let mut spending_inputs = Vec::new(); + for t in &spending_txns { + for (index, input) in enumerate(&t.txn.input) { + if input.prev_hash == funding.txn_id + && input.prev_index == funding.output_index as u32 + { + spending_inputs.push(SpendingInput { + txn_id: t.txn.txid(), + height: t.height, + input_index: index, + }) + } + } + } + assert!(spending_inputs.len() <= 1); + if spending_inputs.len() == 1 { + Some(spending_inputs.remove(0)) } else { None } } - pub fn balance(&self, script_hash: &[u8]) -> f64 { - let mut funding: Vec = self.load_txns( + pub fn status(&self, script_hash: &[u8]) -> Status { + let mut status = Status { + balance: 0, + funding: vec![], + spending: vec![], + }; + + let funding_txns = self.load_txns( self.store .scan(&[b"O", &script_hash[..HASH_PREFIX_LEN]].concat()) .iter() @@ -82,25 +122,27 @@ impl<'a> Query<'a> { }) .collect(), ); - funding.retain(|item| { - item.output - .iter() - .any(|output| compute_script_hash(&output.script_pubkey[..]) == script_hash) - }); - - let mut balance = 0u64; - let mut spending = Vec::::new(); - for txn in &funding { - let txid = txn.txid(); - for (index, output) in enumerate(&txn.output) { - if let Some(spent) = self.find_spending_txn(&txid, index as u32) { - spending.push(spent); // TODO: may contain duplicate TXNs - } else { - balance += output.value; + for t in funding_txns { + let txn_id = t.txn.txid(); + for (index, output) in enumerate(&t.txn.output) { + if compute_script_hash(&output.script_pubkey[..]) == script_hash { + status.funding.push(FundingOutput { + txn_id: txn_id, + height: t.height, + output_index: index, + value: output.value, + }) } } } - balance as f64 / 100_000_000f64 + for funding_output in &status.funding { + if let Some(spent) = self.find_spending_input(&funding_output) { + status.spending.push(spent); + } else { + status.balance += funding_output.value; + } + } + status } pub fn get_tx(&self, tx_hash: &Sha256dHash) -> Bytes { @@ -118,4 +160,9 @@ impl<'a> Query<'a> { } result } + + pub fn get_best_header(&self) -> Option { + let header_list = self.index.headers_list(); + Some(header_list.headers().last()?.clone()) + } } diff --git a/src/rpc.rs b/src/rpc.rs index a043fe8..cbe82c7 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -1,11 +1,15 @@ use bitcoin::util::hash::Sha256dHash; use itertools; use serde_json::{from_str, Number, Value}; +use std::collections::HashMap; use std::io::{BufRead, BufReader, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; -use query::Query; +use query::{Query, Status}; +use index::FullHash; use util; +use crypto::digest::Digest; +use crypto::sha2::Sha256; error_chain!{} @@ -13,9 +17,59 @@ struct Handler<'a> { query: &'a Query<'a>, } +// TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256) +fn hash_from_params(params: &[Value]) -> Result { + let script_hash = params.get(0).chain_err(|| "missing hash")?; + let script_hash = script_hash.as_str().chain_err(|| "non-string hash")?; + let script_hash = Sha256dHash::from_hex(script_hash).chain_err(|| "non-hex hash")?; + Ok(script_hash) +} + +fn history_from_status(status: &Status) -> Vec<(u32, Sha256dHash)> { + let mut txns_map = HashMap::::new(); + for f in &status.funding { + txns_map.insert(f.txn_id, f.height); + } + for s in &status.spending { + txns_map.insert(s.txn_id, s.height); + } + let mut txns: Vec<(u32, Sha256dHash)> = + txns_map.into_iter().map(|item| (item.1, item.0)).collect(); + txns.sort(); + txns +} + +fn hash_from_status(status: &Status) -> Option { + let txns = history_from_status(status); + if txns.is_empty() { + return None; + } + + let mut hash = FullHash::default(); + let mut sha2 = Sha256::new(); + for (height, txn_id) in txns { + let part = format!("{}:{}:", txn_id.be_hex_string(), height); + sha2.input(part.as_bytes()); + } + sha2.result(&mut hash); + Some(hash) +} + impl<'a> Handler<'a> { fn blockchain_headers_subscribe(&self) -> Result { - Ok(json!({})) + let entry = self.query + .get_best_header() + .chain_err(|| "no headers found")?; + let header = entry.header(); + Ok(json!({ + "block_height": entry.height(), + "version": header.version, + "prev_block_hash": header.prev_blockhash.be_hex_string(), + "merkle_root": header.merkle_root.be_hex_string(), + "timestamp": header.time, + "bits": header.bits, + "nonce": header.nonce + })) } fn server_version(&self) -> Result { @@ -52,27 +106,40 @@ impl<'a> Handler<'a> { Ok(json!(1e-5)) // TODO: consult with actual mempool } - fn blockchain_scripthash_subscribe(&self, _params: &[Value]) -> Result { - Ok(json!("HEX_STATUS")) + fn blockchain_relayfee(&self) -> Result { + Ok(json!(1e-5)) // TODO: consult with actual node + } + + fn blockchain_scripthash_subscribe(&self, params: &[Value]) -> Result { + let script_hash = hash_from_params(¶ms).chain_err(|| "bad script_hash")?; + let status = self.query.status(&script_hash[..]); + + Ok(match hash_from_status(&status) { + Some(hash) => Value::String(util::hexlify(&hash)), + None => Value::Null, + }) } fn blockchain_scripthash_get_balance(&self, params: &[Value]) -> Result { - let script_hash = params.get(0).chain_err(|| "missing scripthash")?; - let script_hash = script_hash.as_str().chain_err(|| "non-string scripthash")?; - let script_hash = Sha256dHash::from_hex(script_hash).chain_err(|| "non-hex scripthash")?; - let confirmed = self.query.balance(&script_hash[..]); - Ok(json!({ "confirmed": confirmed })) // TODO: "unconfirmed" + let script_hash = hash_from_params(¶ms).chain_err(|| "bad script_hash")?; + let status = self.query.status(&script_hash[..]); + Ok(json!({ "confirmed": status.balance })) // TODO: "unconfirmed" } - fn blockchain_scripthash_get_history(&self, _params: &[Value]) -> Result { - Ok(json!([])) // TODO: list of {tx_hash: "ABC", height: 123} + fn blockchain_scripthash_get_history(&self, params: &[Value]) -> Result { + let script_hash = hash_from_params(¶ms).chain_err(|| "bad script_hash")?; + let status = self.query.status(&script_hash[..]); + Ok(json!(Value::Array( + history_from_status(&status) + .into_iter() + .map(|item| json!({"height": item.0, "tx_hash": item.1.be_hex_string()})) + .collect() + ))) } fn blockchain_transaction_get(&self, params: &[Value]) -> Result { // TODO: handle 'verbose' param - let tx_hash = params.get(0).chain_err(|| "missing tx_hash")?; - let tx_hash = tx_hash.as_str().chain_err(|| "non-string tx_hash")?; - let tx_hash = Sha256dHash::from_hex(tx_hash).chain_err(|| "non-hex tx_hash")?; + let tx_hash = hash_from_params(params).chain_err(|| "bad tx_hash")?; let tx_hex = util::hexlify(&self.query.get_tx(&tx_hash)); Ok(json!(tx_hex)) } @@ -91,6 +158,7 @@ impl<'a> Handler<'a> { "mempool.get_fee_histogram" => self.mempool_get_fee_histogram(), "blockchain.block.get_chunk" => self.blockchain_block_get_chunk(¶ms), "blockchain.estimatefee" => self.blockchain_estimatefee(¶ms), + "blockchain.relayfee" => self.blockchain_relayfee(), "blockchain.scripthash.subscribe" => self.blockchain_scripthash_subscribe(¶ms), "blockchain.scripthash.get_balance" => self.blockchain_scripthash_get_balance(¶ms), "blockchain.scripthash.get_history" => self.blockchain_scripthash_get_history(¶ms), diff --git a/tools/client.py b/tools/client.py index 783cdff..ad190f0 100644 --- a/tools/client.py +++ b/tools/client.py @@ -14,11 +14,25 @@ script_for_address = BitcoinMainnet.ui.script_for_address log = Logger(__name__) +class Connection: + def __init__(self, addr): + self.s = socket.create_connection(addr) + self.f = self.s.makefile('r') + self.id = 0 + + def call(self, method, *args): + req = { + 'id': self.id, + 'method': method, + 'params': list(args), + } + msg = json.dumps(req) + '\n' + self.s.sendall(msg.encode('ascii')) + return json.loads(self.f.readline()) + def main(): - s = socket.create_connection(('localhost', 50001)) - f = s.makefile('r') - + conn = Connection(('localhost', 50001)) xpub, = sys.argv[1:] total = 0 k = pycoin.ui.key_from_text.key_from_text(xpub) @@ -28,23 +42,20 @@ def main(): address = k.subkey(change).subkey(n).address() script = script_for_address(address) script_hash = hashlib.sha256(script).digest() - req = { - 'id': 1, - 'method': 'blockchain.scripthash.get_balance', - 'params': [script_hash[::-1].hex()] - } - msg = json.dumps(req) + '\n' - s.sendall(msg.encode('ascii')) - res = json.loads(f.readline())['result'] - total += res['confirmed'] - if res['confirmed']: + reply = conn.call('blockchain.scripthash.get_balance', + script_hash[::-1].hex()) + res = reply['result'] + confirmed = res['confirmed'] / 1e8 + total += confirmed + if confirmed: log.info('{}/{} => {} has {:11.8f} BTC', - change, n, address, res['confirmed']) + change, n, address, confirmed) empty = 0 else: empty += 1 if empty >= 10: break + log.info('{}', conn.call('blockchain.scripthash.get_history', script_hash[::-1].hex())) log.info('total balance: {} BTC', total) if __name__ == '__main__':