Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
target
.DS_Store
*db/
*.log
*.sublime*
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,18 @@ See `$ cargo run --release --bin electrs -- --help` for the full list of options
### 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.
the daemon's `getblocktemplate` response unchanged on Bitcoin-compatible chains.
On Liquid, it instead decodes the complete proposal returned by
`getnewblockhex` and projects the recoverable header, transaction, fee,
coinbase, and witness-commitment data into the same response shape. Fields that
have no equivalent mining semantics for signed dynafed blocks use compatibility
defaults or are omitted. The Liquid response is intended for template inspection
and distribution, not block reconstruction or federation signing.

Successful responses are cached for 15 seconds and invalidated early when
electrs indexes a new tip. Cache misses are coalesced into one daemon request.
Responses use `Cache-Control: no-store`, so downstream caches do not extend the
internal lifetime.

## License

Expand Down
110 changes: 98 additions & 12 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,40 @@ pub trait CookieGetter: Send + Sync {
fn get(&self) -> Result<Vec<u8>>;
}

#[derive(Clone)]
struct ConnectionConfig {
addr: SocketAddr,
fallback: Option<SocketAddr>,
cookie_getter: Arc<dyn CookieGetter>,
signal: Waiter,
max_age: Option<Duration>,
}

impl ConnectionConfig {
fn connect(&self) -> Result<Connection> {
Connection::new(
self.addr,
self.fallback,
Arc::clone(&self.cookie_getter),
self.signal.clone(),
self.max_age,
)
}

fn connect_once(&self) -> Result<Connection> {
let (conn, active_addr) = tcp_connect_once(self.addr, self.fallback)?;
Connection::from_stream(
conn,
active_addr,
self.addr,
self.fallback,
Arc::clone(&self.cookie_getter),
self.signal.clone(),
self.max_age,
)
}
}

struct Connection {
tx: TcpStream,
rx: Lines<BufReader<TcpStream>>,
Expand Down Expand Up @@ -516,7 +550,9 @@ pub struct Daemon {
daemon_dir: PathBuf,
blocks_dir: PathBuf,
network: Network,
connection_config: ConnectionConfig,
conn: Mutex<Connection>,
block_template_conn: Mutex<Option<Connection>>,
message_id: Counter, // for monotonic JSONRPC 'id'
signal: Waiter,
conn_max_age: Option<Duration>,
Expand All @@ -542,17 +578,21 @@ impl Daemon {
metrics: &Metrics,
conn_max_age: Option<Duration>,
) -> Result<Daemon> {
let connection_config = ConnectionConfig {
addr: daemon_rpc_addr,
fallback: daemon_rpc_fallback_addr,
cookie_getter,
signal: signal.clone(),
max_age: conn_max_age,
};
let conn = connection_config.connect()?;
let daemon = Daemon {
daemon_dir: daemon_dir.clone(),
blocks_dir: blocks_dir.clone(),
network,
conn: Mutex::new(Connection::new(
daemon_rpc_addr,
daemon_rpc_fallback_addr,
cookie_getter,
signal.clone(),
conn_max_age,
)?),
connection_config,
conn: Mutex::new(conn),
block_template_conn: Mutex::new(None),
message_id: Counter::new(),
signal: signal.clone(),
conn_max_age,
Expand Down Expand Up @@ -616,7 +656,9 @@ impl Daemon {
daemon_dir: self.daemon_dir.clone(),
blocks_dir: self.blocks_dir.clone(),
network: self.network,
connection_config: self.connection_config.clone(),
conn: Mutex::new(self.conn.lock().unwrap().reconnect()?),
block_template_conn: Mutex::new(None),
message_id: Counter::new(),
signal: self.signal.clone(),
conn_max_age: self.conn_max_age,
Expand Down Expand Up @@ -664,8 +706,12 @@ impl Daemon {
}

#[trace]
fn call_jsonrpc(&self, method: &str, request: &Value) -> Result<Value> {
let mut conn = self.conn.lock().unwrap();
fn call_jsonrpc_on_connection(
&self,
method: &str,
request: &Value,
conn: &mut Connection,
) -> Result<Value> {
// Proactively recycle connections older than the configured max age. Re-establishing
// the TCP connection lets a fronting load balancer (e.g. a Kubernetes ClusterSetIP)
// re-select a backend, so a long-lived connection does not stay pinned to a stale
Expand Down Expand Up @@ -711,6 +757,12 @@ impl Daemon {
Ok(result)
}

#[trace]
fn call_jsonrpc(&self, method: &str, request: &Value) -> Result<Value> {
let mut conn = self.conn.lock().unwrap();
self.call_jsonrpc_on_connection(method, request, &mut conn)
}

#[trace(method = %method)]
fn handle_request(&self, method: &str, params: &Value) -> Result<Value> {
let id = self.message_id.next();
Expand Down Expand Up @@ -746,9 +798,32 @@ impl Daemon {
self.retry_request(method, &params)
}

/// Perform a block-template RPC on a connection isolated from singleton RPC users.
/// Connection and warmup failures are returned to the REST caller without retrying.
#[trace]
fn request_no_retry(&self, method: &str, params: Value) -> Result<Value> {
self.handle_request(method, &params)
fn request_block_template_no_retry(&self, method: &str, params: Value) -> Result<Value> {
let id = self.message_id.next();
let req = json!({"method": method, "params": params, "id": id});
let reply = {
let mut slot = self.block_template_conn.lock().unwrap();
if slot.is_none() {
*slot = Some(self.connection_config.connect_once()?);
}
let result = self.call_jsonrpc_on_connection(
method,
&req,
slot.as_mut()
.expect("block-template connection initialized"),
);
if matches!(
result.as_ref().err().map(|err| err.kind()),
Some(ErrorKind::Connection(_))
) {
*slot = None;
}
result?
};
parse_jsonrpc_reply(reply, method, id)
}

#[trace]
Expand Down Expand Up @@ -938,9 +1013,20 @@ impl Daemon {
Ok(serde_json::from_value(res).chain_err(|| "invalid getrawmempool reply")?)
}

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

#[cfg(feature = "liquid")]
#[trace]
pub fn getnewblockhex(&self) -> Result<String> {
let value = self.request_block_template_no_retry("getnewblockhex", json!([]))?;
value
.as_str()
.map(str::to_owned)
.chain_err(|| "non-string getnewblockhex response")
}

#[trace]
Expand Down
Loading
Loading