Skip to main content

light_client/consensus/
payload.rs

1use 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/// Information required to verify a payload.
16#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
17pub struct PayloadProof {
18    /// The payload to be verified.
19    payload: Payload,
20
21    /// VID common data.
22    ///
23    /// This is data necessary to recompute the VID commitment of the payload, which can then be
24    /// verified against a commitment in a previously-verified header.
25    vid_common: VidCommon,
26}
27
28impl PayloadProof {
29    /// Construct a [`PayloadProof`].
30    ///
31    /// Takes the payload to be verified, plus corresponding [`VidCommon`] data to allow a client to
32    /// recompute and verify the commitment of the data.
33    pub fn new(payload: Payload, vid_common: VidCommon) -> Self {
34        Self {
35            payload,
36            vid_common,
37        }
38    }
39
40    /// Inspect the payload without verifying it.
41    pub fn payload(&self) -> &Payload {
42        &self.payload
43    }
44
45    /// Inspect the VID common data the payload commitment is recomputed from.
46    pub fn vid_common(&self) -> &VidCommon {
47        &self.vid_common
48    }
49
50    /// Verify a [`PayloadProof`].
51    ///
52    /// If the data in this proof matches the expected `header`, the full payload data is returned.
53    pub fn verify(self, header: &Header) -> Result<Payload> {
54        Ok(self.verify_with_vid_common(header)?.0)
55    }
56
57    /// Verify a [`PayloadProof`] and get the corresponding [`VidCommon`].
58    ///
59    /// If the data in this proof matches the expected `header`, the full payload data is returned.
60    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                    &param,
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                    &param,
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    /// The commitment is checked against the header's namespace table, so the payload that comes
112    /// back must carry that table too. A provider can pair genuine payload bytes with a different
113    /// table of its own, which would reclassify the transactions into other namespaces.
114    #[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        // The same transaction bytes under a different namespace ID.
135        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}