Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,18 @@ In addition to electrs's original configuration options, a few new options are a
- `--cors <origins>` - origins allowed to make cross-site request (optional, defaults to none).
- `--address-search` - enables the by-prefix address search index.
- `--index-unspendables` - enables indexing of provably unspendable outputs.
- `--enable-mining-rest` - enables cached mining-related HTTP endpoints.
- `--utxos-limit <num>` - maximum number of utxos to return per address.
- `--electrum-txs-limit <num>` - maximum number of txs to return per address in the electrum server (does not apply for the http api).
- `--electrum-banner <text>` - welcome banner text for electrum server.

### Mining-related HTTP endpoints

`GET /block-template` is available only with `--enable-mining-rest`. It proxies
the daemon's `getblocktemplate` template-mode response and caches successful
responses for 15 seconds, invalidating early when electrs indexes a new tip.
Callers that require fresher templates should account for this cache behavior.

Additional options with the `liquid` feature:
- `--parent-network <network>` - the parent network this chain is pegged to.

Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct Config {
pub light_mode: bool,
pub address_search: bool,
pub index_unspendables: bool,
pub enable_mining_rest: bool,
pub cors: Option<String>,
pub precache_scripts: Option<String>,
pub utxos_limit: usize,
Expand Down Expand Up @@ -212,6 +213,11 @@ impl Config {
.long("index-unspendables")
.help("Enable indexing of provably unspendable outputs")
)
.arg(
Arg::with_name("enable_mining_rest")
.long("enable-mining-rest")
.help("Enable cached mining-related HTTP endpoints")
)
.arg(
Arg::with_name("cors")
.long("cors")
Expand Down Expand Up @@ -531,6 +537,7 @@ impl Config {
light_mode: m.is_present("light_mode"),
address_search: m.is_present("address_search"),
index_unspendables: m.is_present("index_unspendables"),
enable_mining_rest: m.is_present("enable_mining_rest"),
cors: m.value_of("cors").map(|s| s.to_string()),
precache_scripts: m.value_of("precache_scripts").map(|s| s.to_string()),
db_block_cache_mb: value_t_or_exit!(m, "db_block_cache_mb", usize),
Expand Down
43 changes: 37 additions & 6 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,7 @@ fn parse_jsonrpc_reply(mut reply: Value, method: &str, expected_id: u64) -> Resu
let msg = err["message"]
.as_str()
.map_or_else(|| err.to_string(), |s| s.to_string());
match code {
// RPC_IN_WARMUP -> retry by later reconnection
-28 => bail!(ErrorKind::Connection(err.to_string())),
code => bail!(ErrorKind::RpcError(code, msg, method.to_string())),
}
bail!(ErrorKind::RpcError(code, msg, method.to_string()))
}
}
}
Expand Down Expand Up @@ -733,6 +729,13 @@ impl Daemon {
*conn = conn.reconnect()?;
continue;
}
Err(e @ Error(ErrorKind::RpcError(-28, _, _), _)) => {
warn!("bitcoind is warming up: {}", e.display_chain());
self.signal.wait(Duration::from_secs(3), false)?;
let mut conn = self.conn.lock().unwrap();
*conn = conn.reconnect()?;
continue;
}
result => return result,
}
}
Expand All @@ -743,6 +746,11 @@ impl Daemon {
self.retry_request(method, &params)
}

#[trace]
fn request_no_retry(&self, method: &str, params: Value) -> Result<Value> {
self.handle_request(method, &params)
}

#[trace]
fn retry_reconnect(&self) -> Daemon {
// XXX add a max reconnection attempts limit?
Expand Down Expand Up @@ -930,6 +938,11 @@ impl Daemon {
Ok(serde_json::from_value(res).chain_err(|| "invalid getrawmempool reply")?)
}

#[trace]
pub fn getblocktemplate(&self, rules: &[&str]) -> Result<Value> {
self.request_no_retry("getblocktemplate", json!([{ "rules": rules }]))
}

#[trace]
pub fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
self.broadcast_raw(&serialize_hex(tx))
Expand Down Expand Up @@ -1080,7 +1093,9 @@ impl Daemon {

#[cfg(test)]
mod tests {
use super::recycle_due;
use super::{parse_jsonrpc_reply, recycle_due};
use crate::errors::{Error, ErrorKind};
use serde_json::json;
use std::time::Duration;

const COOLDOWN: Duration = Duration::from_secs(30);
Expand Down Expand Up @@ -1117,4 +1132,20 @@ mod tests {
fn expired_after_cooldown_retries() {
assert!(recycle_due(secs(600), MAX_AGE, Some(secs(31)), COOLDOWN));
}

#[test]
fn warmup_error_parses_as_rpc_error() {
let reply = json!({
"result": null,
"error": { "code": -28, "message": "warming up" },
"id": 1
});
match parse_jsonrpc_reply(reply, "getblocktemplate", 1) {
Err(Error(ErrorKind::RpcError(-28, message, method), _)) => {
assert_eq!(message, "warming up");
assert_eq!(method, "getblocktemplate");
}
other => panic!("unexpected getblocktemplate warmup result: {:?}", other),
}
}
}
2 changes: 1 addition & 1 deletion src/new_index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub mod zmq;
pub use self::db::{DBRow, DB};
pub use self::fetch::{BlockEntry, FetchFrom};
pub use self::mempool::Mempool;
pub use self::query::Query;
pub use self::query::{Query, GETBLOCKTEMPLATE_TTL};
pub use self::schema::{
compute_script_hash, parse_hash, ChainQuery, FundingInfo, GetAmountVal, Indexer, ScriptStats,
SpendingInfo, SpendingInput, Store, TxHistoryInfo, TxHistoryKey, TxHistoryRow, Utxo,
Expand Down
89 changes: 88 additions & 1 deletion src/new_index/query.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::{BTreeSet, HashMap};
use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard};
use std::time::{Duration, Instant};

use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid};
Expand All @@ -10,6 +10,9 @@ use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo};
use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};

use electrs_macros::trace;
use hyper::body::Bytes as BodyBytes;
use serde_json::Value;
use std::str::FromStr;

#[cfg(feature = "liquid")]
use crate::{
Expand All @@ -18,6 +21,7 @@ use crate::{
};

const FEE_ESTIMATES_TTL: u64 = 60; // seconds
pub const GETBLOCKTEMPLATE_TTL: u64 = 15; // seconds

const CONF_TARGETS: [u16; 28] = [
1u16, 2u16, 3u16, 4u16, 5u16, 6u16, 7u16, 8u16, 9u16, 10u16, 11u16, 12u16, 13u16, 14u16, 15u16,
Expand All @@ -31,10 +35,17 @@ pub struct Query {
config: Arc<Config>,
cached_estimates: RwLock<(HashMap<u16, f64>, Option<Instant>)>,
cached_relayfee: RwLock<Option<f64>>,
cached_block_template: Mutex<Option<CachedBlockTemplate>>,
#[cfg(feature = "liquid")]
asset_db: Option<Arc<RwLock<AssetRegistry>>>,
}

struct CachedBlockTemplate {
tip: crate::chain::BlockHash,
fetched_at: Instant,
body: BodyBytes,
}

impl Query {
#[cfg(not(feature = "liquid"))]
pub fn new(
Expand All @@ -50,6 +61,7 @@ impl Query {
config,
cached_estimates: RwLock::new((HashMap::new(), None)),
cached_relayfee: RwLock::new(None),
cached_block_template: Mutex::new(None),
}
}

Expand Down Expand Up @@ -101,6 +113,43 @@ impl Query {
Ok(result)
}

#[trace]
pub fn getblocktemplate(&self) -> Result<BodyBytes> {
let tip = self.chain.best_hash();

let mut cache = self.cached_block_template.lock().unwrap();
if let Some(cached) = cache.as_ref() {
if cached.tip == tip
&& cached.fetched_at.elapsed() < Duration::from_secs(GETBLOCKTEMPLATE_TTL)
{
return Ok(cached.body.clone());
}
}

let value = self
.daemon
.getblocktemplate(block_template_rules(self.config.network_type))?;
let body = BodyBytes::from(
serde_json::to_string(&value).chain_err(|| "failed to serialize getblocktemplate")?,
);

match block_template_tip(&value) {
Ok(tip) => {
*cache = Some(CachedBlockTemplate {
tip,
fetched_at: Instant::now(),
body: body.clone(),
});
}
Err(err) => {
warn!("not caching getblocktemplate response: {}", err);
*cache = None;
}
}

Ok(body)
}

#[trace]
pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> {
let mut utxos = self.chain.utxo(scripthash, self.config.utxos_limit)?;
Expand Down Expand Up @@ -278,6 +327,7 @@ impl Query {
asset_db,
cached_estimates: RwLock::new((HashMap::new(), None)),
cached_relayfee: RwLock::new(None),
cached_block_template: Mutex::new(None),
}
}

Expand Down Expand Up @@ -311,3 +361,40 @@ impl Query {
Ok((total_num, results))
}
}

#[cfg(not(feature = "liquid"))]
fn block_template_rules(network: Network) -> &'static [&'static str] {
match network {
Network::Signet => &["segwit", "signet"],
_ => &["segwit"],
}
}

#[cfg(feature = "liquid")]
fn block_template_rules(_network: Network) -> &'static [&'static str] {
&["segwit"]
}

fn block_template_tip(value: &Value) -> Result<crate::chain::BlockHash> {
let previousblockhash = value
.get("previousblockhash")
.and_then(|value| value.as_str())
.chain_err(|| "getblocktemplate response missing previousblockhash")?;
crate::chain::BlockHash::from_str(previousblockhash)
.chain_err(|| "invalid getblocktemplate previousblockhash")
}

#[cfg(test)]
mod tests {
use serde_json::json;

#[test]
fn block_template_tip_parses_previousblockhash() {
let hash = "0000000000000000000000000000000000000000000000000000000000000000";
let value = json!({ "previousblockhash": hash });
assert_eq!(super::block_template_tip(&value).unwrap().to_string(), hash);

assert!(super::block_template_tip(&json!({})).is_err());
assert!(super::block_template_tip(&json!({ "previousblockhash": "not a hash" })).is_err());
}
}
Loading
Loading