Skip to main content

tempo_commonware_node/
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 _, StateProviderFactory as _};
16use tempo_chainspec::hardfork::TempoHardforks as _;
17use tempo_node::TempoFullNode;
18use tempo_precompiles::{
19    storage::StorageCtx,
20    validator_config::{IValidatorConfig, ValidatorConfig},
21    validator_config_v2::{IValidatorConfigV2, ValidatorConfigV2},
22};
23
24use tracing::{Level, instrument, warn};
25
26use crate::utils::public_key_to_b256;
27
28/// Returns all active validators read from block state at block `hash`.
29///
30/// The returned validators are those that are marked active according to
31/// block state, and those that are `known`. This accounts for those validators
32/// that are actively participating in consensus (including DKG) but might
33/// be marked inactive on chain.
34///
35/// This function reads the `header` corresponding to `hash` from `node` and
36/// checks if the T2 hardfork is active at `header.timestamp` and if the
37/// Validator Config V2 is initialized.
38///
39/// If T2 is active and the contract is initialized, it will read the entries
40/// from the Validator Config V2 contract.
41///
42/// Otherwise, it will read the entries from the V1 contract.
43pub(crate) fn read_active_and_known_peers_at_block_hash(
44    node: &TempoFullNode,
45    known: &ordered::Set<PublicKey>,
46    hash: B256,
47) -> eyre::Result<ordered::Map<PublicKey, commonware_p2p::Address>> {
48    if can_use_v2_at_block_hash(node, hash, None)
49        .wrap_err("failed to determine validator config v2 status")?
50    {
51        read_active_and_known_peers_at_block_hash_v2(node, known, hash)
52            .wrap_err("failed reading peers from validator config v2")
53    } else {
54        read_active_and_known_peers_at_block_hash_v1(node, known, hash)
55            .wrap_err("failed reading peers from validator config v1")
56    }
57}
58
59/// Returns all validator config v1 entries at block `hash`.
60///
61/// Reads the validator config v1 contract at the block state identified by
62/// `hash` and retains all validators for which `$entry.active = true` or
63/// for which `$entry.publicKey` is in `known`.
64pub(crate) fn read_active_and_known_peers_at_block_hash_v1(
65    node: &TempoFullNode,
66    known: &ordered::Set<PublicKey>,
67    hash: B256,
68) -> eyre::Result<ordered::Map<PublicKey, commonware_p2p::Address>> {
69    read_validator_config_at_block_hash(node, hash, |config: &ValidatorConfig| {
70        let mut all = HashMap::new();
71        for raw in config
72            .get_validators()
73            .wrap_err("failed to query contract for validator config")?
74        {
75            if let Ok(decoded) = DecodedValidatorV1::decode_from_contract(raw)
76                && let Some(dupe) = all.insert(decoded.public_key.clone(), decoded)
77            {
78                warn!(
79                    duplicate = %dupe.public_key,
80                    "found duplicate public keys",
81                );
82            }
83        }
84        all.retain(|k, v| v.active || known.position(k).is_some());
85        Ok(
86            ordered::Map::try_from_iter(all.into_iter().map(|(k, v)| (k, v.to_address())))
87                .expect("hashmaps don't contain duplicates"),
88        )
89    })
90    .map(|(_height, _hash, value)| value)
91}
92
93/// Returns active validator config v2 entries at block `hash`.
94///
95/// This returns both the validators that are `active` as per the contract, and
96/// those that are `known`.
97pub(crate) fn read_active_and_known_peers_at_block_hash_v2(
98    node: &TempoFullNode,
99    known: &ordered::Set<PublicKey>,
100    hash: B256,
101) -> eyre::Result<ordered::Map<PublicKey, commonware_p2p::Address>> {
102    read_validator_config_at_block_hash(node, hash, |config: &ValidatorConfigV2| {
103        let mut all = HashMap::new();
104        for raw in config
105            .get_active_validators()
106            .wrap_err("failed getting active validator set")?
107        {
108            if let Ok(decoded) = DecodedValidatorV2::decode_from_contract(raw)
109                && all
110                    .insert(decoded.public_key.clone(), decoded.to_address())
111                    .is_some()
112            {
113                warn!(
114                    duplicate = %decoded.public_key,
115                    "found duplicate public keys",
116                );
117            }
118        }
119        for member in known {
120            if !all.contains_key(member) {
121                let decoded = config
122                    .validator_by_public_key(public_key_to_b256(member))
123                    .map_err(eyre::Report::new)
124                    .and_then(DecodedValidatorV2::decode_from_contract)
125                    .expect(
126                        "invariant: known peers must have an entry in the \
127                        smart contract and be well formed",
128                    );
129                all.insert(decoded.public_key.clone(), decoded.to_address());
130            }
131        }
132        Ok(ordered::Map::try_from_iter(all).expect("hashmaps don't contain duplicates"))
133    })
134    .map(|(_height, _hash, value)| value)
135}
136
137fn v2_initialization_height_at_block_hash(node: &TempoFullNode, hash: B256) -> eyre::Result<u64> {
138    read_validator_config_at_block_hash(node, hash, |config: &ValidatorConfigV2| {
139        config
140            .get_initialized_at_height()
141            .map_err(eyre::Report::new)
142    })
143    .map(|(_, _, activation_height)| activation_height)
144}
145
146fn is_v2_initialized_at_block_hash(node: &TempoFullNode, hash: B256) -> eyre::Result<bool> {
147    read_validator_config_at_block_hash(node, hash, |config: &ValidatorConfigV2| {
148        config.is_initialized().map_err(eyre::Report::new)
149    })
150    .map(|(_, _, activated)| activated)
151}
152
153/// Reads the validator state at the given block hash.
154pub(crate) fn read_validator_config_at_block_hash<C, T>(
155    node: &TempoFullNode,
156    block_hash: B256,
157    read_fn: impl FnOnce(&C) -> eyre::Result<T>,
158) -> eyre::Result<(u64, B256, T)>
159where
160    C: Default,
161{
162    let header = node
163        .provider
164        .header(block_hash)
165        .map_err(eyre::Report::new)
166        .and_then(|maybe| maybe.ok_or_eyre("execution layer returned empty header"))
167        .wrap_err_with(|| format!("failed reading block with hash `{block_hash}`"))?;
168
169    let db = State::builder()
170        .with_database(StateProviderDatabase::new(
171            node.provider
172                .state_by_block_hash(block_hash)
173                .wrap_err_with(|| {
174                    format!("failed to get state from node provider for hash `{block_hash}`")
175                })?,
176        ))
177        .build();
178
179    let mut evm = node
180        .evm_config
181        .evm_for_block(db, &header)
182        .wrap_err("failed instantiating evm for block")?;
183
184    let ctx = evm.ctx_mut();
185    let res = StorageCtx::enter_evm(
186        &mut ctx.journaled_state,
187        &ctx.block,
188        &ctx.cfg,
189        &ctx.tx,
190        || read_fn(&C::default()),
191    )?;
192    Ok((header.number(), block_hash, res))
193}
194
195/// Returns if the validator config v2 can be used exactly at `hash` and the
196/// timestamp of the corresponding `header`.
197///
198/// If `latest` is set, then the function will look at the timestamp of `hash`
199/// to determine hardfork activation, but `latest` to determine contract
200/// initialization. This is an optimization that makes use of the fact that
201/// the initialization height is stored in the contract.
202///
203/// Validators can be read from the V2 contract if the following conditions hold:
204///
205/// 1. `timestamp(hash) >= T2`.
206/// 2. `initialization_height(<state>) <= number(hash)`.
207/// 3. `is_init(<state>) == true`.
208///
209/// `<state>` is read at either `hash` or `latest` if set.
210///
211/// This makes use of the following assumption:
212///
213/// If `initialization_height > 0`, then `is_init == true` always (invariant of
214/// the smart contract).
215///
216/// If `initialization_height == 0`, then `is_init` is used to determine if
217/// the contract was initialized at genesis or not.
218pub(crate) fn can_use_v2_at_block_hash(
219    node: &TempoFullNode,
220    hash: B256,
221    latest: Option<B256>,
222) -> eyre::Result<bool> {
223    let header = node
224        .provider
225        .header(hash)
226        .map_err(eyre::Report::new)
227        .and_then(|maybe| maybe.ok_or_eyre("hash not known"))
228        .wrap_err_with(|| {
229            format!("failed reading header for block hash `{hash}` from execution layer")
230        })?;
231    let state_hash = latest.unwrap_or(hash);
232    Ok(node
233        .chain_spec()
234        .is_t2_active_at_timestamp(header.timestamp())
235        && is_v2_initialized_at_block_hash(node, state_hash)
236            .wrap_err("failed reading validator config v2 initialization flag")?
237        && v2_initialization_height_at_block_hash(node, state_hash)
238            .wrap_err("failed reading validator config v2 initialization height")?
239            <= header.number())
240}
241
242/// A ContractValidator is a peer read from the validator config smart const.
243///
244/// The inbound and outbound addresses stored herein are guaranteed to be of the
245/// form `<host>:<port>` for inbound, and `<ip>:<port>` for outbound. Here,
246/// `<host>` is either an IPv4 or IPV6 address, or a fully qualified domain name.
247/// `<ip>` is an IPv4 or IPv6 address.
248#[derive(Clone, Debug, PartialEq, Eq)]
249pub(crate) struct DecodedValidatorV1 {
250    pub(crate) active: bool,
251    /// The `publicKey` field of the contract. Used by other validators to
252    /// identify a peer by verifying the signatures of its p2p messages and
253    /// as a dealer/player/participant in DKG ceremonies and consensus for a
254    /// given epoch. Part of the set registered with the lookup p2p manager.
255    pub(crate) public_key: PublicKey,
256    /// The `inboundAddress` field of the contract. Used by other validators
257    /// to dial a peer and ensure that messages from that peer are coming from
258    /// this address. Part of the set registered with the lookup p2p manager.
259    pub(crate) inbound: SocketAddr,
260    /// The `outboundAddress` field of the contract. Currently ignored because
261    /// all p2p communication is symmetric (outbound and inbound) via the
262    /// `inboundAddress` field.
263    pub(crate) outbound: SocketAddr,
264    /// The `index` field of the contract. Not used by consensus and just here
265    /// for debugging purposes to identify the contract entry. Emitted in
266    /// tracing events.
267    pub(crate) index: u64,
268    /// The `address` field of the contract. Not used by consensus and just here
269    /// for debugging purposes to identify the contract entry. Emitted in
270    /// tracing events.
271    pub(crate) address: Address,
272}
273
274impl DecodedValidatorV1 {
275    /// Attempts to decode a single validator from the values read in the smart contract.
276    ///
277    /// This function does not perform hostname lookup on either of the addresses.
278    /// Instead, only the shape of the addresses are checked for whether they are
279    /// socket addresses (IP:PORT pairs), or fully qualified domain names.
280    #[instrument(ret(Display, level = Level::DEBUG), err(level = Level::WARN))]
281    fn decode_from_contract(
282        IValidatorConfig::Validator {
283            active,
284            publicKey,
285            index,
286            validatorAddress,
287            inboundAddress,
288            outboundAddress,
289        }: IValidatorConfig::Validator,
290    ) -> eyre::Result<Self> {
291        let public_key = PublicKey::decode(publicKey.as_ref())
292            .wrap_err("failed decoding publicKey field as ed25519 public key")?;
293        let inbound = inboundAddress
294            .parse()
295            .wrap_err("inboundAddress was not valid")?;
296        let outbound = outboundAddress
297            .parse()
298            .wrap_err("outboundAddress was not valid")?;
299        Ok(Self {
300            active,
301            public_key,
302            inbound,
303            outbound,
304            index,
305            address: validatorAddress,
306        })
307    }
308
309    fn to_address(&self) -> commonware_p2p::Address {
310        // NOTE: commonware takes egress as socket address but only uses the IP part.
311        // So setting port to 0 is ok.
312        commonware_p2p::Address::Asymmetric {
313            ingress: Ingress::Socket(self.inbound),
314            egress: self.outbound,
315        }
316    }
317}
318
319impl std::fmt::Display for DecodedValidatorV1 {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        f.write_fmt(format_args!(
322            "public key = `{}`, inbound = `{}`, outbound = `{}`, index = `{}`, address = `{}`",
323            self.public_key, self.inbound, self.outbound, self.index, self.address
324        ))
325    }
326}
327
328/// An entry in the validator config v2 contract with all its fields decoded
329/// into Rust types.
330pub(crate) struct DecodedValidatorV2 {
331    public_key: PublicKey,
332    ingress: SocketAddr,
333    egress: IpAddr,
334    added_at_height: u64,
335    deleted_at_height: u64,
336    index: u64,
337    address: Address,
338}
339
340impl DecodedValidatorV2 {
341    #[instrument(ret(Display, level = Level::DEBUG), err(level = Level::WARN))]
342    pub(crate) fn decode_from_contract(
343        IValidatorConfigV2::Validator {
344            publicKey,
345            validatorAddress: address,
346            ingress,
347            egress,
348            index,
349            addedAtHeight: added_at_height,
350            deactivatedAtHeight: deleted_at_height,
351            ..
352        }: IValidatorConfigV2::Validator,
353    ) -> eyre::Result<Self> {
354        let public_key = PublicKey::decode(publicKey.as_ref())
355            .wrap_err("failed decoding publicKey field as ed25519 public key")?;
356        let ingress = ingress.parse().wrap_err("ingress was not valid")?;
357        let egress = egress.parse().wrap_err("egress was not valid")?;
358        Ok(Self {
359            public_key,
360            ingress,
361            egress,
362            added_at_height,
363            deleted_at_height,
364            index,
365            address,
366        })
367    }
368
369    fn to_address(&self) -> commonware_p2p::Address {
370        // NOTE: commonware takes egress as socket address but only uses the IP part.
371        // So setting port to 0 is ok.
372        commonware_p2p::Address::Asymmetric {
373            ingress: Ingress::Socket(self.ingress),
374            egress: SocketAddr::from((self.egress, 0)),
375        }
376    }
377}
378impl std::fmt::Display for DecodedValidatorV2 {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        f.write_fmt(format_args!(
381            "public key = `{}`, ingress = `{}`, egress = `{}`, added_at_height: `{}`, deleted_at_height = `{}`, index = `{}`, address = `{}`",
382            self.public_key,
383            self.ingress,
384            self.egress,
385            self.added_at_height,
386            self.deleted_at_height,
387            self.index,
388            self.address
389        ))
390    }
391}