Skip to main content

decdn_incentive/
deployment.rs

1//! The `PaymentPool` deployment a piece of pool state belongs to.
2//!
3//! `poolId = keccak256(owner, ownerPoolNonce)` names neither the chain nor the
4//! contract, and a redeploy restarts every owner's nonce, so a pool id repeats
5//! across deployments. Every persisted row that names a pool id therefore also
6//! carries the [`Deployment`] that wrote it: the seller-side lane store keeps
7//! one stamp for all its rows, and each buyer pool row carries its own tag
8//! ([`crate::buyer_pool::BuyerPoolState::deployment`]).
9
10use alloy::primitives::Address;
11use alloy::sol_types::Eip712Domain;
12
13use crate::store::StoreError;
14use crate::voucher::voucher_domain;
15
16/// The `PaymentPool` deployment a row belongs to: the chain and contract
17/// address that make up the voucher EIP-712 domain.
18///
19/// A row is only meaningful against the deployment that wrote it. Two
20/// deployments that share a contract address on different chains, or a chain
21/// with different contract addresses, are different deployments.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct Deployment {
24    /// EIP-712 `chainId` of the voucher domain.
25    pub chain_id: u64,
26    /// The `PaymentPool` contract, the voucher domain's `verifyingContract`.
27    /// The nonzero check lives at the config boundary
28    /// (`decdn_common::address::parse_nonzero_address`); a new construction
29    /// site validates there, not here.
30    pub payment_pool: Address,
31}
32
33impl Deployment {
34    /// Byte width of an encoded [`Deployment`]: `8 + 20`.
35    pub const ENCODED_LEN: usize = 28;
36
37    /// The voucher EIP-712 domain of this deployment.
38    #[must_use]
39    pub fn voucher_domain(self) -> Eip712Domain {
40        voucher_domain(self.chain_id, self.payment_pool)
41    }
42
43    /// Encode as `chain_id (8 bytes, big-endian) ‖ payment_pool (20 bytes)`.
44    #[must_use]
45    pub fn to_bytes(self) -> [u8; Self::ENCODED_LEN] {
46        let mut out = [0u8; Self::ENCODED_LEN];
47        out[..8].copy_from_slice(&self.chain_id.to_be_bytes());
48        out[8..].copy_from_slice(self.payment_pool.as_slice());
49        out
50    }
51
52    /// Decode the [`Self::to_bytes`] form.
53    ///
54    /// # Errors
55    ///
56    /// [`StoreError::Corrupt`] for any width other than [`Self::ENCODED_LEN`]:
57    /// a deployment decides which rows a reader trusts, so bytes this binary
58    /// cannot read must not decode to some deployment.
59    pub fn from_bytes(bytes: &[u8]) -> Result<Self, StoreError> {
60        let corrupt = || StoreError::Corrupt {
61            pool_id: None,
62            detail: format!(
63                "deployment is {} bytes, expected {}",
64                bytes.len(),
65                Self::ENCODED_LEN
66            ),
67        };
68        let bytes: &[u8; Self::ENCODED_LEN] = bytes.try_into().map_err(|_| corrupt())?;
69        // Width is proven above; these splits re-prove it to the compiler
70        // without reaching for a panicking accessor.
71        let (chain, pool) = bytes.split_first_chunk::<8>().ok_or_else(corrupt)?;
72        let pool: &[u8; 20] = pool.try_into().map_err(|_| corrupt())?;
73        Ok(Self {
74            chain_id: u64::from_be_bytes(*chain),
75            payment_pool: Address::from(*pool),
76        })
77    }
78}
79
80#[cfg(test)]
81mod tests;