use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use crate::composition::CompositionRecord;
use crate::error::{CompositionError, EncodingError};
use crate::primitives::{
anchor::Anchor, evidence::Evidence, predicate::Predicate,
revelation_mask::RevelationMask, subject::Subject, temporal::TemporalFrame,
};
#[cfg(feature = "transcript-v2")]
use crate::transcript_v2::TranscriptVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ClaimRefAlg {
#[serde(rename = "sha-256")]
Sha256,
#[serde(rename = "poseidon-felt252")]
PoseidonFelt252,
}
impl Default for ClaimRefAlg {
fn default() -> Self {
Self::Sha256
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClaimRef {
#[serde(with = "serde_bytes")]
pub digest: Vec<u8>,
#[serde(rename = "digest-alg", default, skip_serializing_if = "Option::is_none")]
pub digest_alg: Option<ClaimRefAlg>,
}
impl ClaimRef {
pub fn from_digest(digest: Vec<u8>) -> Self {
Self {
digest,
digest_alg: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Claim {
pub subject: Subject,
pub predicate: Predicate,
pub evidence: Evidence,
#[serde(rename = "temporal-frame")]
pub temporal_frame: TemporalFrame,
#[serde(rename = "revelation-mask")]
pub revelation_mask: RevelationMask,
pub anchor: Anchor,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub composition: Option<CompositionRecord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
#[cfg(feature = "transcript-v2")]
#[serde(
rename = "transcript-version",
default,
skip_serializing_if = "is_default_transcript_version"
)]
pub transcript_version: TranscriptVersion,
}
#[cfg(feature = "transcript-v2")]
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_default_transcript_version(v: &TranscriptVersion) -> bool {
*v == TranscriptVersion::V1
}
#[cfg(feature = "transcript-v2")]
pub enum TranscriptResult {
V1(merlin::Transcript),
V2(crate::transcript_v2::TranscriptT2),
}
#[cfg(feature = "transcript-v2")]
impl core::fmt::Debug for TranscriptResult {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::V1(_) => write!(f, "TranscriptResult::V1(<Merlin>)"),
Self::V2(_) => write!(f, "TranscriptResult::V2(<TranscriptT2>)"),
}
}
}
impl Claim {
pub fn validate(&self) -> Result<(), CompositionError> {
self.subject.validate_shape()?;
self.predicate.validate_shape()?;
self.evidence.validate_shape()?;
self.temporal_frame.validate_shape()?;
self.revelation_mask.validate_shape()?;
for e in &self.anchor.0 {
e.validate_shape()?;
}
if self.anchor.0.is_empty() {
return Err(CompositionError::Invariant(
"anchor list must be non-empty (I-4)",
));
}
Ok(())
}
pub fn claim_ref(&self) -> Result<ClaimRef, EncodingError> {
let bytes = crate::codec::to_cbor_canonical(self)?;
let digest = sha2::Sha256::digest(&bytes);
Ok(ClaimRef {
digest: digest.to_vec(),
digest_alg: Some(ClaimRefAlg::Sha256),
})
}
pub fn claim_ref_with(&self, alg: ClaimRefAlg) -> Result<ClaimRef, EncodingError> {
let bytes = crate::codec::to_cbor_canonical(self)?;
let digest = match alg {
ClaimRefAlg::Sha256 => sha2::Sha256::digest(&bytes).to_vec(),
ClaimRefAlg::PoseidonFelt252 => {
#[cfg(feature = "poseidon")]
{
crate::poseidon::poseidon_felt252(&bytes).to_vec()
}
#[cfg(not(feature = "poseidon"))]
{
sha2::Sha256::digest(&bytes).to_vec()
}
}
};
Ok(ClaimRef {
digest,
digest_alg: Some(alg),
})
}
pub fn is_atomic(&self) -> bool {
self.composition.is_none()
}
pub fn depth(&self) -> u64 {
self.composition.as_ref().map_or(0, |c| c.depth)
}
#[cfg(feature = "transcript-v2")]
pub fn verify_with_transcript(
&self,
verifier_ctx: &[u8],
nonce: &[u8],
required_version: TranscriptVersion,
) -> Result<TranscriptResult, crate::error::TranscriptError> {
if self.transcript_version != required_version {
return Err(crate::error::TranscriptError::VersionMismatch {
claim: alloc::format!("{:?}", self.transcript_version),
expected: alloc::format!("{required_version:?}"),
});
}
match required_version {
TranscriptVersion::V1 => {
let t = crate::transcript::bind_claim(self, verifier_ctx, nonce)?;
Ok(TranscriptResult::V1(t))
}
TranscriptVersion::V2 => {
let t = crate::transcript_v2::bind_claim_v2(self, verifier_ctx, nonce)?;
Ok(TranscriptResult::V2(t))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::ClaimBuilder;
use crate::primitives::anchor::{Anchor, AnchorEntry, AnchorType};
use crate::primitives::evidence::{Evidence, EvidenceScheme};
use crate::primitives::predicate::{Predicate, PredicateType};
use crate::primitives::revelation_mask::RevelationMask;
use crate::primitives::subject::{Subject, SubjectId, SubjectType};
use crate::primitives::temporal::TemporalFrame;
fn fixture_claim() -> Claim {
ClaimBuilder::default()
.subject(Subject {
subject_type: SubjectType::Wallet,
id: SubjectId::Bytes(hex::decode(
"bbbe91b88ff2842d7f7af15cd8154cdcc753dc3997e46be3568e7ef1ab5e90f4",
)
.unwrap()),
binding: None,
})
.predicate(
Predicate::new(
PredicateType::Equality,
"vauban.claim",
b"body".to_vec(),
)
.unwrap(),
)
.evidence(
Evidence::new(EvidenceScheme::Ed25519, vec![0xcd; 64], None).unwrap(),
)
.temporal_frame(
TemporalFrame::new(1_700_000_000, None, None).unwrap(),
)
.revelation_mask(
RevelationMask::new(vec!["*".into()], vec![], None).unwrap(),
)
.anchor(
Anchor::new(vec![AnchorEntry {
anchor_type: AnchorType::StarknetL3,
r#ref: vec![0xab; 32],
epoch: Some(42),
nullifier: None,
meta: None,
}])
.unwrap(),
)
.build()
.expect("fixture claim")
}
#[test]
fn claim_ref_sha256_determinism() {
let c = fixture_claim();
let r1 = c.claim_ref().unwrap();
let r2 = c.claim_ref().unwrap();
assert_eq!(r1.digest, r2.digest);
assert_eq!(r1.digest.len(), 32); assert_eq!(r1.digest_alg, Some(ClaimRefAlg::Sha256));
}
#[test]
fn claim_ref_sha256_explicit() {
let c = fixture_claim();
let r1 = c.claim_ref().unwrap();
let r2 = c.claim_ref_with(ClaimRefAlg::Sha256).unwrap();
assert_eq!(r1.digest, r2.digest);
}
#[test]
fn claim_ref_different_inputs_different_digests() {
let c1 = fixture_claim();
let mut c2 = fixture_claim();
c2.predicate = Predicate::new(
PredicateType::Equality,
"different.domain",
b"different".to_vec(),
)
.unwrap();
let r1 = c1.claim_ref().unwrap();
let r2 = c2.claim_ref().unwrap();
assert_ne!(r1.digest, r2.digest);
}
#[test]
fn claim_ref_poseidon_determinism() {
let c = fixture_claim();
let r1 = c.claim_ref_with(ClaimRefAlg::PoseidonFelt252).unwrap();
let r2 = c.claim_ref_with(ClaimRefAlg::PoseidonFelt252).unwrap();
assert_eq!(r1.digest, r2.digest);
assert_eq!(r1.digest.len(), 32); }
#[test]
fn claim_ref_poseidon_vs_sha256_independent() {
let c = fixture_claim();
let r_sha = c.claim_ref().unwrap();
let r_poseidon = c.claim_ref_with(ClaimRefAlg::PoseidonFelt252).unwrap();
assert_eq!(r_sha.digest_alg, Some(ClaimRefAlg::Sha256));
#[cfg(feature = "poseidon")]
{
assert_ne!(r_sha.digest, r_poseidon.digest);
assert_eq!(r_poseidon.digest.len(), 32);
}
#[cfg(not(feature = "poseidon"))]
{
assert_eq!(r_poseidon.digest, r_sha.digest);
}
}
}