use std::collections::BTreeMap;
use bitcoin::{CompactTarget, ScriptBuf, Target};
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::federation::Federation;
use crate::parents::{resolve_parent, Family, Parent};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Peg {
pub txid: String,
pub vout: u32,
pub amount: u64,
pub script: String,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChainDocument {
pub id: String,
pub name: String,
pub parent: String,
pub challenge: String,
pub pow_limit: String,
pub address_prefix: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub magic: Option<String>,
#[serde(default = "default_peg_confirmations")]
pub peg_confirmations: u32,
#[serde(default = "default_refund_blocks")]
pub refund_blocks: u32,
#[serde(default = "default_pegout_blocks")]
pub pegout_blocks: u32,
#[serde(default = "default_pegout_min")]
pub pegout_min: u64,
#[serde(default = "default_min_fee_rate")]
pub min_fee_rate: u64,
pub genesis_time: u32,
#[serde(default)]
pub pegs: Vec<Peg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub genesis_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signers: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub threshold: Option<u32>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
fn default_peg_confirmations() -> u32 {
6
}
fn default_refund_blocks() -> u32 {
10_000
}
fn default_pegout_blocks() -> u32 {
144
}
fn default_pegout_min() -> u64 {
10_000
}
fn default_min_fee_rate() -> u64 {
1
}
pub fn magic_for(name: &str) -> String {
let h = format!("sidestr:{name}")
.bytes()
.fold(7u32, |h, b| h.wrapping_mul(31).wrapping_add(u32::from(b)));
format!("{h:08x}")
}
fn is_hex(s: &str, len: usize) -> bool {
s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit())
}
impl ChainDocument {
pub fn from_json(text: &str) -> Result<Self> {
let doc: Self = serde_json::from_str(text)?;
doc.validate()?;
Ok(doc)
}
pub fn to_json(&self) -> Result<String> {
let mut out = Vec::new();
let fmt = serde_json::ser::PrettyFormatter::with_indent(b" ");
let mut ser = serde_json::Serializer::with_formatter(&mut out, fmt);
self.serialize(&mut ser)?;
let mut s = String::from_utf8(out).map_err(|e| Error::Encoding(e.to_string()))?;
s.push('\n');
Ok(s)
}
pub fn validate(&self) -> Result<()> {
let bad = |m: String| Err(Error::Document(m));
self.parent()?;
if self.id.is_empty() || self.name.is_empty() {
return bad("id and name are required".into());
}
if self.challenge.len() < 2
|| self.challenge.len() % 2 != 0
|| !is_hex(&self.challenge, self.challenge.len())
{
return bad("challenge must be a non-empty hex script".into());
}
if !is_hex(&self.pow_limit, 64) {
return bad("powLimit must be 64 hex characters".into());
}
if self.address_prefix.is_empty()
|| self.address_prefix.len() > 8
|| !self.address_prefix.bytes().all(|b| b.is_ascii_lowercase())
{
return bad("addressPrefix is 1 to 8 lower-case letters".into());
}
if let Some(m) = &self.magic {
if !is_hex(m, 8) {
return bad("magic must be 8 hex characters".into());
}
}
if let Some(h) = &self.genesis_hash {
if !is_hex(h, 64) {
return bad("genesisHash must be 64 hex characters".into());
}
}
for (i, p) in self.pegs.iter().enumerate() {
if !is_hex(&p.txid, 64)
|| p.script.is_empty()
|| p.script.len() % 2 != 0
|| !is_hex(&p.script, p.script.len())
{
return bad(format!(
"peg {i}: txid must be 64 hex characters and script a hex script"
));
}
}
if let Some(s) = &self.signer {
if !is_hex(s, 64) {
return bad("signer must be a 64-hex x-only public key".into());
}
let c = self.challenge.to_ascii_lowercase();
if c.starts_with("5120") && c.len() == 68 && c[4..] != s.to_ascii_lowercase() {
return bad("signer is not the key the challenge names".into());
}
}
if let Some(rules) = &self.rules {
if let Some(r) = rules.iter().find(|r| !r.is_empty()) {
return bad(format!("chain {} names rule \"{r}\", which this validator does not have (sidestr-core carries the core rules only)", self.id));
}
}
Federation::for_document(self)?;
Ok(())
}
pub fn parent(&self) -> Result<&'static Parent> {
resolve_parent(&self.parent)
}
pub fn family(&self) -> Result<Family> {
Ok(self.parent()?.family)
}
pub fn challenge_script(&self) -> Result<ScriptBuf> {
Ok(ScriptBuf::from_bytes(
hex::decode(&self.challenge).map_err(|e| Error::Encoding(e.to_string()))?,
))
}
pub fn pow_limit_target(&self) -> Result<Target> {
let bytes: [u8; 32] = hex::decode(&self.pow_limit)
.map_err(|e| Error::Encoding(e.to_string()))?
.try_into()
.map_err(|_| Error::Document("powLimit must be 32 bytes".into()))?;
Ok(Target::from_be_bytes(bytes))
}
pub fn bits(&self) -> Result<CompactTarget> {
Ok(self.pow_limit_target()?.to_compact_lossy())
}
pub fn derived_magic(&self) -> String {
magic_for(&self.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TRIAL: &str = include_str!("../fixtures/trial/chain.json");
const DREAMLAB: &str = include_str!("../fixtures/dreamlab/chain.json");
#[test]
fn fixtures_parse_and_round_trip() {
for (text, name, magic) in [
(TRIAL, "trial", "a8f6706f"),
(DREAMLAB, "dreamlab", "d981eab1"),
] {
let doc = ChainDocument::from_json(text).unwrap();
assert_eq!(doc.name, name);
assert_eq!(doc.magic.as_deref(), Some(magic));
assert_eq!(doc.derived_magic(), magic);
assert_eq!(doc.bits().unwrap().to_consensus(), 0x207f_ffff);
let again: serde_json::Value = serde_json::from_str(&doc.to_json().unwrap()).unwrap();
assert_eq!(
again,
serde_json::from_str::<serde_json::Value>(text).unwrap()
);
}
let d = ChainDocument::from_json(DREAMLAB).unwrap();
assert_eq!(d.extra["containment"]["parent"], "tbtc4");
}
#[test]
fn refusals() {
let base: serde_json::Value = serde_json::from_str(TRIAL).unwrap();
let with = |f: &dyn Fn(&mut serde_json::Value)| {
let mut v = base.clone();
f(&mut v);
ChainDocument::from_json(&v.to_string())
};
let e = with(&|v| v["rules"] = serde_json::json!(["assets", "oracle"]))
.unwrap_err()
.to_string();
assert!(e.contains("does not have"), "{e}");
assert_eq!(
with(&|v| v["parent"] = "txbt4".into())
.unwrap()
.family()
.unwrap(),
Family::Blake2b
);
assert!(matches!(
with(&|v| v["parent"] = "doge".into()),
Err(Error::UnknownParent(_))
));
assert!(with(&|v| v["signers"] = serde_json::json!(["aa"])).is_err());
assert!(with(&|v| {
v["signers"] = serde_json::json!(["aa".repeat(32)]);
v["threshold"] = serde_json::json!(1);
})
.unwrap_err()
.to_string()
.contains("is not the one 1 signers"));
assert!(with(&|v| v["signer"] = serde_json::json!("00".repeat(32))).is_err());
assert!(with(&|v| v["powLimit"] = serde_json::json!("ff")).is_err());
assert!(with(&|v| v["addressPrefix"] = serde_json::json!("TRL")).is_err());
assert!(with(&|v| v["genesisHash"] = serde_json::json!("zz")).is_err());
}
}