#![cfg(feature = "net")]
use crate::{
compute_fold_digest, data::digest_from_hex, data::digest_to_hex,
julian::JULIAN_GENESIS_STATEMENT, AnchorMetadata, EntryAnchor, LedgerAnchor,
};
use serde::{Deserialize, Serialize};
use std::{env, error::Error, fmt};
pub const SCHEMA_ANCHOR: &str = "mfenx.powerhouse.anchor.v1";
pub const SCHEMA_ENVELOPE: &str = "mfenx.powerhouse.envelope.v1";
pub const SCHEMA_VOTE: &str = "mfenx.powerhouse.vote.v1";
pub const ENVELOPE_SCHEMA_VERSION: u32 = 1;
pub const NETWORK_ID: &str = "MFENX-POWERHOUSE";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AnchorEntryJson {
pub statement: String,
pub hashes: Vec<String>,
#[serde(default)]
pub merkle_root: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AnchorJson {
pub schema: String,
pub network: String,
pub node_id: String,
pub genesis: String,
pub entries: Vec<AnchorEntryJson>,
pub quorum: usize,
pub timestamp_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub challenge_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fold_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crate_version: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub da_commitments: Vec<DaCommitmentJson>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evidence_root: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DaCommitmentJson {
pub namespace: String,
pub blob_hash: String,
pub share_root: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pedersen_root: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub da_provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub da_commitment: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub da_height: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub da_status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attestation_qc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AnchorEnvelope {
pub schema: String,
#[serde(default = "default_envelope_version")]
pub schema_version: u32,
pub public_key: String,
pub node_id: String,
pub payload: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AnchorVoteJson {
pub schema: String,
pub network: String,
pub round: u64,
pub anchor_hash: String,
pub public_key: String,
pub signature: String,
}
#[derive(Debug, Clone)]
pub enum AnchorCodecError {
InvalidSchema {
expected: &'static str,
found: String,
},
InvalidNetwork {
expected: &'static str,
found: String,
},
MissingGenesis,
InvalidDigest {
entry: usize,
reason: String,
},
}
impl fmt::Display for AnchorCodecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSchema { expected, found } => {
write!(f, "invalid schema: expected {expected}, found {found}")
}
Self::InvalidNetwork { expected, found } => {
write!(f, "invalid network: expected {expected}, found {found}")
}
Self::MissingGenesis => write!(f, "ledger anchor missing JULIAN genesis entry"),
Self::InvalidDigest { entry, reason } => {
write!(
f,
"ledger anchor entry {entry} has invalid digest: {reason}"
)
}
}
}
}
impl Error for AnchorCodecError {}
impl AnchorJson {
pub fn from_ledger(
node_id: impl Into<String>,
quorum: usize,
anchor: &LedgerAnchor,
timestamp_ms: u64,
da_commitments: Vec<DaCommitmentJson>,
evidence_root: Option<String>,
) -> Result<Self, AnchorCodecError> {
if anchor.entries.is_empty()
|| anchor.entries.first().map(|e| e.statement.as_str())
!= Some(JULIAN_GENESIS_STATEMENT)
{
return Err(AnchorCodecError::MissingGenesis);
}
let entries = anchor
.entries
.iter()
.map(|entry| AnchorEntryJson {
statement: entry.statement.clone(),
hashes: entry.hashes.iter().map(digest_to_hex).collect(),
merkle_root: Some(digest_to_hex(&entry.merkle_root)),
})
.collect();
let fold_digest = anchor
.metadata
.fold_digest
.unwrap_or_else(|| compute_fold_digest(anchor));
Ok(Self {
schema: SCHEMA_ANCHOR.to_string(),
network: NETWORK_ID.to_string(),
node_id: node_id.into(),
genesis: JULIAN_GENESIS_STATEMENT.to_string(),
entries,
quorum,
timestamp_ms,
challenge_mode: anchor.metadata.challenge_mode.clone(),
fold_digest: Some(digest_to_hex(&fold_digest)),
crate_version: anchor.metadata.crate_version.clone(),
da_commitments,
evidence_root,
})
}
pub fn into_ledger(self) -> Result<LedgerAnchor, AnchorCodecError> {
if self.schema != SCHEMA_ANCHOR {
return Err(AnchorCodecError::InvalidSchema {
expected: SCHEMA_ANCHOR,
found: self.schema,
});
}
if self.network != NETWORK_ID {
return Err(AnchorCodecError::InvalidNetwork {
expected: NETWORK_ID,
found: self.network,
});
}
if self.entries.first().map(|e| e.statement.as_str()) != Some(JULIAN_GENESIS_STATEMENT) {
return Err(AnchorCodecError::MissingGenesis);
}
let mut entries = Vec::with_capacity(self.entries.len());
for (idx, entry) in self.entries.into_iter().enumerate() {
let mut hashes = Vec::with_capacity(entry.hashes.len());
for hash_str in entry.hashes {
let digest = digest_from_hex(&hash_str)
.map_err(|reason| AnchorCodecError::InvalidDigest { entry: idx, reason })?;
hashes.push(digest);
}
let merkle_root = if let Some(root_hex) = entry.merkle_root {
digest_from_hex(&root_hex)
.map_err(|reason| AnchorCodecError::InvalidDigest { entry: idx, reason })?
} else {
crate::merkle_root(&hashes)
};
entries.push(EntryAnchor {
statement: entry.statement,
hashes,
merkle_root,
});
}
let mut metadata = AnchorMetadata {
challenge_mode: self.challenge_mode,
crate_version: self
.crate_version
.or_else(|| Some(env!("CARGO_PKG_VERSION").to_string())),
..AnchorMetadata::default()
};
if let Some(fold_hex) = self.fold_digest {
metadata.fold_digest = Some(
digest_from_hex(&fold_hex)
.map_err(|reason| AnchorCodecError::InvalidDigest { entry: 0, reason })?,
);
}
if metadata.fold_digest.is_none() {
let temp = LedgerAnchor {
entries: entries.clone(),
metadata: AnchorMetadata::default(),
};
metadata.fold_digest = Some(compute_fold_digest(&temp));
}
Ok(LedgerAnchor { entries, metadata })
}
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json_str(input: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(input)
}
}
impl AnchorEnvelope {
pub fn validate(&self) -> Result<(), AnchorCodecError> {
if self.schema != SCHEMA_ENVELOPE {
return Err(AnchorCodecError::InvalidSchema {
expected: SCHEMA_ENVELOPE,
found: self.schema.clone(),
});
}
if self.schema_version > ENVELOPE_SCHEMA_VERSION {
return Err(AnchorCodecError::InvalidSchema {
expected: "schema_version <= current",
found: format!("{}", self.schema_version),
});
}
Ok(())
}
}
impl AnchorVoteJson {
pub fn validate(&self) -> Result<(), AnchorCodecError> {
if self.schema != SCHEMA_VOTE {
return Err(AnchorCodecError::InvalidSchema {
expected: SCHEMA_VOTE,
found: self.schema.clone(),
});
}
if self.network != NETWORK_ID {
return Err(AnchorCodecError::InvalidNetwork {
expected: NETWORK_ID,
found: self.network.clone(),
});
}
if self.anchor_hash.len() != 64 || !self.anchor_hash.chars().all(|c| c.is_ascii_hexdigit())
{
return Err(AnchorCodecError::InvalidDigest {
entry: 0,
reason: "invalid anchor_hash".to_string(),
});
}
Ok(())
}
}
fn default_envelope_version() -> u32 {
ENVELOPE_SCHEMA_VERSION
}