light_client/consensus/
payload.rs1use anyhow::{Context, Result, anyhow, ensure};
2use espresso_types::{Header, Payload};
3use hotshot_types::{
4 data::{VidCommitment, VidCommon, ns_table::parse_ns_table},
5 traits::block_contents::EncodeBytes,
6 vid::{
7 advz::{ADVZScheme, advz_scheme},
8 avidm::{AvidMScheme, init_avidm_param},
9 avidm_gf2::{AvidmGf2Scheme, init_avidm_gf2_param},
10 },
11};
12use jf_advz::VidScheme;
13use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
17pub struct PayloadProof {
18 payload: Payload,
20
21 vid_common: VidCommon,
26}
27
28impl PayloadProof {
29 pub fn new(payload: Payload, vid_common: VidCommon) -> Self {
34 Self {
35 payload,
36 vid_common,
37 }
38 }
39
40 pub fn payload(&self) -> &Payload {
42 &self.payload
43 }
44
45 pub fn vid_common(&self) -> &VidCommon {
47 &self.vid_common
48 }
49
50 pub fn verify(self, header: &Header) -> Result<Payload> {
54 Ok(self.verify_with_vid_common(header)?.0)
55 }
56
57 pub fn verify_with_vid_common(self, header: &Header) -> Result<(Payload, VidCommon)> {
61 ensure!(
62 self.payload.ns_table() == header.ns_table(),
63 "namespace table of payload does not match namespace table in header"
64 );
65 let commit = match &self.vid_common {
66 VidCommon::V0(common) => {
67 advz_scheme(ADVZScheme::get_num_storage_nodes(common) as usize)
68 .commit_only(self.payload.encode())
69 .map(VidCommitment::V0)
70 .context("computing ADVZ commitment")?
71 },
72 VidCommon::V1(avidm) => {
73 let param = init_avidm_param(avidm.total_weights)?;
74 let bytes = self.payload.encode();
75 AvidMScheme::commit(
76 ¶m,
77 &bytes,
78 parse_ns_table(bytes.len(), &header.ns_table().encode()),
79 )
80 .map(VidCommitment::V1)
81 .map_err(|err| anyhow!("computing AvidM commitment: {err:#}"))?
82 },
83 VidCommon::V2(avidm_gf2) => {
84 let param = init_avidm_gf2_param(avidm_gf2.param.total_weights)?;
85 let bytes = self.payload.encode();
86 AvidmGf2Scheme::commit(
87 ¶m,
88 &bytes,
89 parse_ns_table(bytes.len(), &header.ns_table().encode()),
90 )
91 .map(|(comm, _)| VidCommitment::V2(comm))
92 .map_err(|err| anyhow!("computing AvidM commitment: {err:#}"))?
93 },
94 };
95 ensure!(
96 commit == header.payload_commitment(),
97 "commitment of payload does not match commitment in header"
98 );
99 Ok((self.payload, self.vid_common))
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use espresso_types::{NamespaceId, NodeState, Transaction};
106 use hotshot_types::traits::block_contents::BlockPayload;
107
108 use super::*;
109 use crate::testing::TestClient;
110
111 #[tokio::test]
115 #[test_log::test]
116 async fn test_verify_rejects_payload_with_forged_ns_table() {
117 let client = TestClient::default();
118 let tx = vec![1, 2, 3];
119 client
120 .add_block(
121 1,
122 vec![Transaction::new(NamespaceId::from(1u32), tx.clone())],
123 )
124 .await;
125 let header = client.leaf(1).await.header().clone();
126 let payload = client.payload(1).await;
127 let vid_common = client.vid_common(1).await;
128
129 let verified = PayloadProof::new(payload.clone(), vid_common.clone())
130 .verify(&header)
131 .unwrap();
132 assert_eq!(verified.ns_table(), header.ns_table());
133
134 let other = Payload::from_transactions_sync(
136 &[Transaction::new(NamespaceId::from(2u32), tx)],
137 NodeState::mock_v3().chain_config,
138 )
139 .unwrap()
140 .0;
141 assert_ne!(other.ns_table(), payload.ns_table());
142 let forged = Payload::from_bytes(payload.raw_payload(), other.ns_table());
143
144 let err = PayloadProof::new(forged, vid_common)
145 .verify(&header)
146 .unwrap_err();
147 assert!(err.to_string().contains("namespace table"), "{err:#}");
148 }
149}