use chia_protocol::Bytes32;
use chia_wallet_sdk::driver::MetadataWithRootHash;
use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, Raw, ToClvm, ToClvmError};
use crate::size::SizeBucket;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DigDataStoreMetadata {
pub root_hash: Bytes32,
pub label: Option<String>,
pub description: Option<String>,
pub size_proof: Option<String>,
pub program_hash: Option<Bytes32>,
pub size_bucket: Option<SizeBucket>,
}
impl<N, E: ClvmEncoder<Node = N>> ToClvm<E> for DigDataStoreMetadata {
fn to_clvm(&self, encoder: &mut E) -> Result<N, ToClvmError> {
let mut items: Vec<(&str, Raw<N>)> = Vec::new();
if let Some(label) = &self.label {
items.push(("l", Raw(label.to_clvm(encoder)?)));
}
if let Some(description) = &self.description {
items.push(("d", Raw(description.to_clvm(encoder)?)));
}
if let Some(size_proof) = &self.size_proof {
items.push(("sp", Raw(size_proof.to_clvm(encoder)?)));
}
if let Some(program_hash) = self.program_hash {
items.push(("p", Raw(program_hash.to_clvm(encoder)?)));
}
if let Some(size_bucket) = self.size_bucket {
items.push(("sz", Raw(size_bucket.exponent().to_clvm(encoder)?)));
}
(self.root_hash, items).to_clvm(encoder)
}
}
impl<N, D: ClvmDecoder<Node = N>> FromClvm<D> for DigDataStoreMetadata {
fn from_clvm(decoder: &D, node: N) -> Result<Self, FromClvmError> {
let (root_hash, items) = <(Bytes32, Vec<(String, Raw<N>)>)>::from_clvm(decoder, node)?;
let mut metadata = Self::root_hash_only(root_hash);
for (key, Raw(ptr)) in items {
match key.as_str() {
"l" => metadata.label = Some(String::from_clvm(decoder, ptr)?),
"d" => metadata.description = Some(String::from_clvm(decoder, ptr)?),
"sp" => metadata.size_proof = Some(String::from_clvm(decoder, ptr)?),
"p" => metadata.program_hash = Some(Bytes32::from_clvm(decoder, ptr)?),
"sz" => metadata.size_bucket = Some(decode_size_bucket(decoder, ptr)?),
_ => (),
}
}
Ok(metadata)
}
}
fn decode_size_bucket<N, D: ClvmDecoder<Node = N>>(
decoder: &D,
ptr: N,
) -> Result<SizeBucket, FromClvmError> {
const INVALID_SZ: &str = "invalid sz: non-minimal or out-of-range size exponent";
let atom = decoder.decode_atom(&ptr)?;
let exponent = match atom.as_ref() {
[] => 0u8,
[byte] if (1..=10).contains(byte) => *byte,
_ => return Err(FromClvmError::Custom(INVALID_SZ.to_string())),
};
SizeBucket::from_exponent(exponent).map_err(|error| FromClvmError::Custom(error.to_string()))
}
impl MetadataWithRootHash for DigDataStoreMetadata {
fn root_hash(&self) -> Bytes32 {
self.root_hash
}
fn root_hash_only(root_hash: Bytes32) -> Self {
Self {
root_hash,
label: None,
description: None,
size_proof: None,
program_hash: None,
size_bucket: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chia_wallet_sdk::driver::{DataStoreMetadata, SpendContext};
use chia_wallet_sdk::prelude::{Allocator, NodePtr};
use clvm_traits::{FromClvm, ToClvm};
fn clvm_bytes<T: ToClvm<Allocator>>(value: &T) -> Vec<u8> {
let mut ctx = SpendContext::new();
let node = value.to_clvm(&mut *ctx).expect("encode value");
let program = chia_protocol::Program::from_clvm(&*ctx, node).expect("node to program");
program.as_ref().to_vec()
}
fn sample_fields() -> (Bytes32, Option<String>, Option<String>, Option<String>) {
(
Bytes32::new([0xcd; 32]),
Some("site".into()),
Some("a description".into()),
Some("size-proof".into()),
)
}
fn sz_atom_bytes(metadata: &DigDataStoreMetadata) -> Vec<u8> {
let mut ctx = SpendContext::new();
let node = metadata.to_clvm(&mut *ctx).expect("encode");
let (_root, items) =
<(Bytes32, Vec<(String, Raw<NodePtr>)>)>::from_clvm(&*ctx, node).expect("decode alist");
let (_key, Raw(ptr)) = items
.into_iter()
.find(|(k, _)| k == "sz")
.expect("sz key present");
chia_protocol::Bytes::from_clvm(&*ctx, ptr)
.expect("sz atom")
.into_inner()
}
#[test]
fn metadata_none_size_bucket_is_byte_identical_to_sdk() {
let (root, label, description, size_proof) = sample_fields();
let dig = DigDataStoreMetadata {
root_hash: root,
label: label.clone(),
description: description.clone(),
size_proof: size_proof.clone(),
program_hash: None,
size_bucket: None,
};
let sdk = DataStoreMetadata {
root_hash: root,
label,
description,
bytes: None,
size_proof,
};
assert_eq!(
clvm_bytes(&dig),
clvm_bytes(&sdk),
"a None size_bucket/program_hash store must serialize byte-identically to the SDK metadata"
);
}
#[test]
fn sdk_reader_parses_size_bucket_store() {
let (root, label, description, size_proof) = sample_fields();
let dig = DigDataStoreMetadata {
root_hash: root,
label: label.clone(),
description: description.clone(),
size_proof: size_proof.clone(),
program_hash: None,
size_bucket: Some(SizeBucket::from_exponent(5).unwrap()),
};
let mut ctx = SpendContext::new();
let node = dig.to_clvm(&mut *ctx).expect("encode dig");
let sdk = DataStoreMetadata::from_clvm(&*ctx, node).expect("sdk decodes, ignoring sz");
assert_eq!(sdk.root_hash, root);
assert_eq!(sdk.label, label);
assert_eq!(sdk.description, description);
assert_eq!(sdk.size_proof, size_proof);
assert_eq!(sdk.bytes, None, "no b key so SDK reads bytes as None");
}
#[test]
fn sz_atom_is_minimally_encoded() {
let with_k = |k: u8| DigDataStoreMetadata {
root_hash: Bytes32::new([0x01; 32]),
label: None,
description: None,
size_proof: None,
program_hash: None,
size_bucket: Some(SizeBucket::from_exponent(k).unwrap()),
};
assert_eq!(
sz_atom_bytes(&with_k(0)),
Vec::<u8>::new(),
"k=0 empty atom"
);
assert_eq!(sz_atom_bytes(&with_k(5)), vec![0x05], "k=5 single byte");
assert_eq!(sz_atom_bytes(&with_k(10)), vec![0x0a], "k=10 single byte");
}
#[test]
fn sz_roundtrips() {
let (root, label, description, size_proof) = sample_fields();
let metadata = DigDataStoreMetadata {
root_hash: root,
label,
description,
size_proof,
program_hash: Some(Bytes32::new([0x11; 32])),
size_bucket: Some(SizeBucket::from_exponent(7).unwrap()),
};
let mut ctx = SpendContext::new();
let node = metadata.to_clvm(&mut *ctx).expect("encode");
let decoded = DigDataStoreMetadata::from_clvm(&*ctx, node).expect("decode");
assert_eq!(decoded, metadata, "size_bucket and all fields round-trip");
}
#[test]
fn sz_decode_rejects_non_minimal_and_oversized() {
let build_with_raw_sz = |raw: Vec<u8>| -> Result<DigDataStoreMetadata, FromClvmError> {
let mut ctx = SpendContext::new();
let root = Bytes32::new([0x01; 32]);
let sz_node = chia_protocol::Bytes::new(raw).to_clvm(&mut *ctx).unwrap();
let items: Vec<(&str, Raw<NodePtr>)> = vec![("sz", Raw(sz_node))];
let node = (root, items).to_clvm(&mut *ctx).unwrap();
DigDataStoreMetadata::from_clvm(&*ctx, node)
};
assert!(
build_with_raw_sz(vec![0x00]).is_err(),
"leading-zero rejected"
);
assert!(
build_with_raw_sz(vec![0x00, 0x05]).is_err(),
"multi-byte non-minimal rejected"
);
assert!(
build_with_raw_sz(vec![0x0b]).is_err(),
"exponent 11 rejected"
);
assert!(matches!(
SizeBucket::from_exponent(11),
Err(crate::MerkleError::InvalidSize(_))
));
}
#[test]
fn sz_is_last_key() {
let metadata = DigDataStoreMetadata {
root_hash: Bytes32::new([0xab; 32]),
label: Some("l".into()),
description: Some("d".into()),
size_proof: Some("sp".into()),
program_hash: Some(Bytes32::new([0x01; 32])),
size_bucket: Some(SizeBucket::from_exponent(3).unwrap()),
};
let mut ctx = SpendContext::new();
let node = metadata.to_clvm(&mut *ctx).expect("encode");
let (_root, items) =
<(Bytes32, Vec<(String, Raw<NodePtr>)>)>::from_clvm(&*ctx, node).expect("decode");
let keys: Vec<&str> = items.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(
keys,
vec!["l", "d", "sp", "p", "sz"],
"sz is appended last, no b"
);
}
#[test]
fn sdk_store_with_b_still_decodes() {
let decode_sdk_bytes = |bytes: Option<u64>| -> DigDataStoreMetadata {
let mut ctx = SpendContext::new();
let sdk = DataStoreMetadata {
root_hash: Bytes32::new([0x02; 32]),
label: Some("sdk".into()),
description: None,
bytes,
size_proof: Some("sp".into()),
};
let node = sdk.to_clvm(&mut *ctx).expect("encode sdk");
DigDataStoreMetadata::from_clvm(&*ctx, node).expect("decode sdk store")
};
let with_b = decode_sdk_bytes(Some(4096));
assert_eq!(with_b.root_hash, Bytes32::new([0x02; 32]));
assert_eq!(
with_b.label,
Some("sdk".into()),
"other fields still decode"
);
assert_eq!(with_b.size_proof, Some("sp".into()));
assert_eq!(
with_b.size_bucket, None,
"a foreign b is ignored, not read as the size"
);
assert_eq!(decode_sdk_bytes(None).size_bucket, None);
}
}