use crate::imp::core::{Bytes32, ChiaBlockRef};
use crate::imp::prover::chain::ChainSource;
use crate::imp::prover::error::{ProverError, Result};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct BlockRecord {
pub header_hash: String,
pub height: u32,
#[serde(default)]
pub timestamp: Option<u64>,
#[serde(default)]
pub prev_transaction_block_height: Option<u32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BlockchainState {
pub peak: Option<BlockRecord>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BlockchainStateResponse {
pub success: bool,
#[serde(default)]
pub blockchain_state: Option<BlockchainState>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BlockRecordResponse {
pub success: bool,
#[serde(default)]
pub block_record: Option<BlockRecord>,
#[serde(default)]
pub error: Option<String>,
}
pub type BlockRecordResolver<'a> = dyn FnMut(u32) -> Result<BlockRecord> + 'a;
fn parse_header_hash(s: &str) -> Result<Bytes32> {
let hex = s.strip_prefix("0x").unwrap_or(s);
Bytes32::from_hex(hex).map_err(|e| ProverError::ChainRpc(format!("bad header hash hex: {e:?}")))
}
fn record_to_ref(
record: BlockRecord,
resolve: &mut BlockRecordResolver<'_>,
) -> Result<ChiaBlockRef> {
let header_hash = parse_header_hash(&record.header_hash)?;
let height = record.height;
let timestamp = resolve_timestamp(&record, resolve)?;
Ok(ChiaBlockRef {
header_hash,
height,
timestamp,
})
}
fn resolve_timestamp(record: &BlockRecord, resolve: &mut BlockRecordResolver<'_>) -> Result<u64> {
if let Some(ts) = record.timestamp {
return Ok(ts);
}
let mut next = record.prev_transaction_block_height;
for _ in 0..256 {
let h = next.ok_or_else(|| {
ProverError::ChainRpc(format!(
"block at height {} has no timestamp and no prev_transaction_block_height",
record.height
))
})?;
let prev = resolve(h)?;
if let Some(ts) = prev.timestamp {
return Ok(ts);
}
next = prev.prev_transaction_block_height;
}
Err(ProverError::ChainRpc(format!(
"could not resolve a transaction block timestamp for height {}",
record.height
)))
}
pub fn parse_blockchain_state(resp: BlockchainStateResponse) -> Result<ChiaBlockRef> {
if !resp.success {
return Err(ProverError::ChainRpc(
resp.error
.unwrap_or_else(|| "get_blockchain_state failed".into()),
));
}
let peak = resp
.blockchain_state
.and_then(|s| s.peak)
.ok_or_else(|| ProverError::ChainRpc("no peak in blockchain_state".into()))?;
record_to_ref(peak, &mut |_h| {
Err(ProverError::ChainRpc(
"peak unexpectedly lacked a timestamp".into(),
))
})
}
pub fn parse_block_record_resolved(
resp: BlockRecordResponse,
resolve: &mut BlockRecordResolver<'_>,
) -> Result<ChiaBlockRef> {
if !resp.success {
return Err(ProverError::ChainRpc(
resp.error
.unwrap_or_else(|| "get_block_record failed".into()),
));
}
let rec = resp
.block_record
.ok_or_else(|| ProverError::ChainRpc("no block_record in response".into()))?;
record_to_ref(rec, resolve)
}
#[derive(Debug, Clone)]
pub struct CoinsetChainSource {
base_url: String,
client: reqwest::blocking::Client,
}
impl Default for CoinsetChainSource {
fn default() -> Self {
Self::new("https://api.coinset.org")
}
}
impl CoinsetChainSource {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
client: reqwest::blocking::Client::new(),
}
}
fn post_json(&self, endpoint: &str, body: String) -> Result<String> {
let url = format!("{}/{}", self.base_url.trim_end_matches('/'), endpoint);
let resp = self
.client
.post(&url)
.header("content-type", "application/json")
.body(body)
.send()
.map_err(|e| ProverError::ChainRpc(format!("{endpoint} send: {e}")))?;
resp.text()
.map_err(|e| ProverError::ChainRpc(format!("{endpoint} body: {e}")))
}
fn fetch_record(&self, height: u32) -> Result<BlockRecord> {
let body = self.post_json(
"get_block_record_by_height",
format!("{{\"height\": {height}}}"),
)?;
let resp: BlockRecordResponse = serde_json::from_str(&body)
.map_err(|e| ProverError::ChainRpc(format!("parse record: {e}")))?;
if !resp.success {
return Err(ProverError::ChainRpc(
resp.error
.unwrap_or_else(|| "get_block_record_by_height failed".into()),
));
}
resp.block_record
.ok_or_else(|| ProverError::ChainRpc("no block_record in response".into()))
}
}
impl ChainSource for CoinsetChainSource {
fn get_peak(&self) -> Result<ChiaBlockRef> {
let body = self.post_json("get_blockchain_state", "{}".into())?;
let resp: BlockchainStateResponse = serde_json::from_str(&body)
.map_err(|e| ProverError::ChainRpc(format!("parse state: {e}")))?;
parse_blockchain_state(resp)
}
fn verify_block(&self, block: &ChiaBlockRef, freshness_window_secs: u64) -> Result<()> {
let body = self.post_json(
"get_block_record_by_height",
format!("{{\"height\": {}}}", block.height),
)?;
let resp: BlockRecordResponse = serde_json::from_str(&body)
.map_err(|e| ProverError::ChainRpc(format!("parse record: {e}")))?;
let on_chain = parse_block_record_resolved(resp, &mut |h| self.fetch_record(h))?;
if on_chain.header_hash != block.header_hash {
return Err(ProverError::BlockNotOnChain(block.header_hash.to_hex()));
}
let now = self.get_peak()?.timestamp;
if block.timestamp > now {
return Err(ProverError::BlockInFuture(block.timestamp, now));
}
if now - block.timestamp > freshness_window_secs {
return Err(ProverError::BlockTooOld {
block_ts: block.timestamp,
now,
window: freshness_window_secs,
});
}
Ok(())
}
}