Skip to main content

tempo_consensus/
validators.rs

1use std::{
2    collections::HashMap,
3    net::{IpAddr, SocketAddr},
4};
5
6use alloy_consensus::BlockHeader;
7use alloy_primitives::{Address, B256};
8use commonware_codec::DecodeExt as _;
9use commonware_cryptography::ed25519::PublicKey;
10use commonware_p2p::Ingress;
11use commonware_utils::{TryFromIterator, ordered};
12use eyre::{OptionExt as _, WrapErr as _};
13use reth_ethereum::evm::revm::{State, database::StateProviderDatabase};
14use reth_node_builder::ConfigureEvm as _;
15use reth_provider::{HeaderProvider as _, StateProviderBox, StateProviderFactory as _};
16use tempo_node::{TempoFullNode, evm::evm::TempoEvm};
17use tempo_precompiles::{
18    storage::StorageCtx,
19    validator_config_v2::{IValidatorConfigV2, ValidatorConfigV2},
20};
21use tempo_primitives::TempoHeader;
22
23use tracing::{Level, debug, instrument, warn};
24
25use crate::utils::public_key_to_b256;
26
27/// Minimal execution-node interface needed to read validator config state.
28///
29/// Production code uses [`TempoFullNode`]. This trait exists so unit tests can
30/// use a mock that only provides a historical state provider and an EVM
31/// configured for the corresponding block, while still exercising the same
32/// validator config reader used in production.
33pub(crate) trait ExecutionNode {
34    fn header(&self, block_hash: B256) -> eyre::Result<TempoHeader>;
35
36    fn state_by_block_hash(&self, block_hash: B256) -> eyre::Result<StateProviderBox>;
37
38    fn evm_for_block(
39        &self,
40        db: State<StateProviderDatabase<StateProviderBox>>,
41        header: &TempoHeader,
42    ) -> eyre::Result<TempoEvm<State<StateProviderDatabase<StateProviderBox>>>>;
43}
44
45impl ExecutionNode for TempoFullNode {
46    fn header(&self, block_hash: B256) -> eyre::Result<TempoHeader> {
47        self.provider
48            .header(block_hash)
49            .map_err(eyre::Report::new)
50            .and_then(|maybe| maybe.ok_or_eyre("execution layer returned empty header"))
51    }
52
53    fn state_by_block_hash(&self, block_hash: B256) -> eyre::Result<StateProviderBox> {
54        self.provider
55            .state_by_block_hash(block_hash)
56            .map_err(eyre::Report::new)
57    }
58
59    fn evm_for_block(
60        &self,
61        db: State<StateProviderDatabase<StateProviderBox>>,
62        header: &TempoHeader,
63    ) -> eyre::Result<TempoEvm<State<StateProviderDatabase<StateProviderBox>>>> {
64        self.evm_config
65            .evm_for_block(db, header)
66            .map_err(eyre::Report::new)
67    }
68}
69
70impl<N> ExecutionNode for &N
71where
72    N: ExecutionNode,
73{
74    fn header(&self, block_hash: B256) -> eyre::Result<TempoHeader> {
75        (*self).header(block_hash)
76    }
77
78    fn state_by_block_hash(&self, block_hash: B256) -> eyre::Result<StateProviderBox> {
79        (*self).state_by_block_hash(block_hash)
80    }
81
82    fn evm_for_block(
83        &self,
84        db: State<StateProviderDatabase<StateProviderBox>>,
85        header: &TempoHeader,
86    ) -> eyre::Result<TempoEvm<State<StateProviderDatabase<StateProviderBox>>>> {
87        (*self).evm_for_block(db, header)
88    }
89}
90
91/// Returns active validator config v2 entries at block `hash`.
92///
93/// This returns both the validators that are `active` as per the contract, and
94/// those that are `known`.
95pub(crate) fn read_active_and_known_peers_at_block_hash(
96    node: impl ExecutionNode,
97    known: &ordered::Set<PublicKey>,
98    hash: B256,
99) -> eyre::Result<ordered::Map<PublicKey, commonware_p2p::Address>> {
100    read_validator_config_at_block_hash(node, hash, |config: &ValidatorConfigV2| {
101        let mut all = HashMap::new();
102        for raw in config
103            .get_active_validators()
104            .wrap_err("failed getting active validator set")?
105        {
106            if let Ok(decoded) = DecodedValidatorV2::decode_from_contract(raw)
107                && all
108                    .insert(decoded.public_key.clone(), decoded.to_p2p_address())
109                    .is_some()
110            {
111                warn!(
112                    duplicate = %decoded.public_key,
113                    "found duplicate public keys",
114                );
115            }
116        }
117        debug!(
118            active_validators = ?all,
119            "read active validators from contract, now extending with historic \
120            peers that are still in the peer set but no longer marked active",
121        );
122        for member in known {
123            if !all.contains_key(member) {
124                let decoded = config
125                    .validator_by_public_key(public_key_to_b256(member))
126                    .map_err(eyre::Report::new)
127                    .and_then(DecodedValidatorV2::decode_from_contract)
128                    .wrap_err_with(|| format!("failed to read peer `{member}` from contract"))
129                    .expect(
130                        "invariant: known peers must have an entry in the \
131                        smart contract and be well formed",
132                    );
133                all.insert(decoded.public_key.clone(), decoded.to_p2p_address());
134            }
135        }
136        Ok(ordered::Map::try_from_iter(all).expect("hashmaps don't contain duplicates"))
137    })
138    .map(|(_height, _hash, value)| value)
139}
140
141/// Reads the validator state at the given block hash.
142#[instrument(skip_all, fields(%block_hash), err(Display))]
143pub(crate) fn read_validator_config_at_block_hash<C, T>(
144    node: impl ExecutionNode,
145    block_hash: B256,
146    read_fn: impl FnOnce(&C) -> eyre::Result<T>,
147) -> eyre::Result<(u64, B256, T)>
148where
149    C: Default,
150{
151    let header = node
152        .header(block_hash)
153        .wrap_err_with(|| format!("failed reading block with hash `{block_hash}`"))?;
154
155    debug!(height = header.number(), "header found");
156
157    let db = State::builder()
158        .with_database(StateProviderDatabase::new(
159            node.state_by_block_hash(block_hash).wrap_err_with(|| {
160                format!("failed to get state from node provider for hash `{block_hash}`")
161            })?,
162        ))
163        .build();
164
165    let mut evm = node
166        .evm_for_block(db, &header)
167        .wrap_err("failed instantiating evm for block")?;
168
169    let ctx = evm.ctx_mut();
170    let res = StorageCtx::enter_evm(
171        &mut ctx.journaled_state,
172        &ctx.block,
173        &ctx.cfg,
174        &ctx.tx,
175        || read_fn(&C::default()),
176    )?;
177    Ok((header.number(), block_hash, res))
178}
179
180/// An entry in the validator config v2 contract with all its fields decoded
181/// into Rust types.
182pub(crate) struct DecodedValidatorV2 {
183    public_key: PublicKey,
184    ingress: SocketAddr,
185    egress: IpAddr,
186    added_at_height: u64,
187    deleted_at_height: u64,
188    index: u64,
189    address: Address,
190}
191
192impl DecodedValidatorV2 {
193    #[instrument(ret(Display, level = Level::DEBUG), err(level = Level::WARN))]
194    pub(crate) fn decode_from_contract(
195        IValidatorConfigV2::Validator {
196            publicKey,
197            validatorAddress: address,
198            ingress,
199            egress,
200            index,
201            addedAtHeight: added_at_height,
202            deactivatedAtHeight: deleted_at_height,
203            ..
204        }: IValidatorConfigV2::Validator,
205    ) -> eyre::Result<Self> {
206        let public_key = PublicKey::decode(publicKey.as_ref())
207            .wrap_err("failed decoding publicKey field as ed25519 public key")?;
208        let ingress = ingress.parse().wrap_err("ingress was not valid")?;
209        let egress = egress.parse().wrap_err("egress was not valid")?;
210        Ok(Self {
211            public_key,
212            ingress,
213            egress,
214            added_at_height,
215            deleted_at_height,
216            index,
217            address,
218        })
219    }
220
221    pub(crate) fn public_key(&self) -> &PublicKey {
222        &self.public_key
223    }
224
225    pub(crate) fn to_p2p_address(&self) -> commonware_p2p::Address {
226        // NOTE: commonware takes egress as socket address but only uses the IP part.
227        // So setting port to 0 is ok.
228        commonware_p2p::Address::Asymmetric {
229            ingress: Ingress::Socket(self.ingress),
230            egress: SocketAddr::from((self.egress, 0)),
231        }
232    }
233}
234impl std::fmt::Display for DecodedValidatorV2 {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        f.write_fmt(format_args!(
237            "public key = `{}`, ingress = `{}`, egress = `{}`, added_at_height: `{}`, deleted_at_height = `{}`, index = `{}`, address = `{}`",
238            self.public_key,
239            self.ingress,
240            self.egress,
241            self.added_at_height,
242            self.deleted_at_height,
243            self.index,
244            self.address
245        ))
246    }
247}