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;