Skip to main content

foundry_common/tempo/
hardfork.rs

1//! Tempo hardfork activation queries.
2
3use crate::{FoundryTransactionBuilder, provider::is_rpc_method_not_found};
4use alloy_network::{Network, TransactionBuilder};
5use alloy_provider::Provider;
6use alloy_transport::{TransportError, TransportErrorKind};
7use eyre::Result;
8use serde::Deserialize;
9use tempo_alloy::{chainspec::hardfork::TempoHardfork, rpc::ForkSchedule};
10use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
11
12#[derive(Debug, Deserialize)]
13#[serde(rename_all = "camelCase")]
14struct AnvilNodeInfo {
15    hard_fork: Option<String>,
16    network: Option<String>,
17}
18
19/// Returns whether `hardfork` is active at the head of the chain behind `provider`.
20///
21/// Queries `tempo_forkSchedule` and falls back to `anvil_nodeInfo` for Anvil nodes, which do not
22/// serve the fork schedule. Fails when neither source reports a hardfork known to this build.
23pub async fn is_tempo_hardfork_active<N: Network, P: Provider<N>>(
24    provider: &P,
25    hardfork: TempoHardfork,
26) -> Result<bool> {
27    Ok(active_tempo_hardfork(provider).await? >= hardfork)
28}
29
30/// Returns the Tempo hardfork active on the RPC, falling back to `anvil_nodeInfo` for nodes that
31/// do not serve the fork schedule.
32pub async fn active_tempo_hardfork<N: Network, P: Provider<N>>(
33    provider: &P,
34) -> Result<TempoHardfork> {
35    match fork_schedule_hardfork(provider).await {
36        Ok(hardfork) => Ok(hardfork),
37        Err(err) if is_rpc_method_not_found(&err) => match anvil_tempo_hardfork(provider).await {
38            Ok(Some(hardfork)) => Ok(hardfork),
39            _ => Err(err.into()),
40        },
41        Err(err) => Err(err.into()),
42    }
43}
44
45/// Fails when `tx` carries a non-zero expiring nonce and the chain is known to predate T12.
46///
47/// TIP-1106 turns the nonce of an expiring nonce transaction into an opaque discriminator from
48/// T12 on. Earlier hardforks reject any value other than zero. When the active hardfork cannot be
49/// determined the check is skipped and validation is left to the node.
50pub async fn ensure_expiring_nonce_discriminator_active<N: Network, P: Provider<N>>(
51    provider: &P,
52    tx: &N::TransactionRequest,
53) -> Result<()>
54where
55    N::TransactionRequest: FoundryTransactionBuilder<N>,
56{
57    if tx.nonce_key() == Some(TEMPO_EXPIRING_NONCE_KEY)
58        && let Some(nonce) = tx.nonce().filter(|nonce| *nonce != 0)
59        && matches!(is_tempo_hardfork_active(provider, TempoHardfork::T12).await, Ok(false))
60    {
61        eyre::bail!(
62            "expiring nonce transactions must use nonce 0 before the Tempo T12 hardfork, got nonce {nonce}; non-zero expiring nonce discriminators (TIP-1106) are not active on this chain"
63        );
64    }
65    Ok(())
66}
67
68async fn fork_schedule_hardfork<N: Network, P: Provider<N>>(
69    provider: &P,
70) -> Result<TempoHardfork, TransportError> {
71    let schedule = provider.raw_request::<_, ForkSchedule>("tempo_forkSchedule".into(), ()).await?;
72    schedule.active.parse::<TempoHardfork>().map_err(TransportErrorKind::custom)
73}
74
75async fn anvil_tempo_hardfork<N: Network, P: Provider<N>>(
76    provider: &P,
77) -> Result<Option<TempoHardfork>, TransportError> {
78    let info = provider.raw_request::<_, AnvilNodeInfo>("anvil_nodeInfo".into(), ()).await?;
79    Ok(hardfork_from_anvil_node_info(&info))
80}
81
82fn hardfork_from_anvil_node_info(info: &AnvilNodeInfo) -> Option<TempoHardfork> {
83    if info.network.as_deref() != Some("tempo") {
84        return None;
85    }
86    info.hard_fork.as_deref()?.parse().ok()
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use alloy_json_rpc::ErrorPayload;
93    use alloy_primitives::U256;
94    use alloy_provider::{ProviderBuilder, mock::Asserter};
95    use tempo_alloy::{TempoNetwork, rpc::TempoTransactionRequest};
96
97    fn mocked_provider(asserter: Asserter) -> impl Provider<TempoNetwork> {
98        ProviderBuilder::new().network::<TempoNetwork>().connect_mocked_client(asserter)
99    }
100
101    #[tokio::test]
102    async fn tempo_fork_schedule_detects_t3_activation() {
103        for (active, expected) in [("T2", false), ("T3", true), ("T13", true), ("T14", true)] {
104            let asserter = Asserter::new();
105            asserter.push_success(&serde_json::json!({ "active": active, "schedule": [] }));
106            let provider = mocked_provider(asserter);
107            assert_eq!(
108                is_tempo_hardfork_active(&provider, TempoHardfork::T3).await.unwrap(),
109                expected
110            );
111        }
112    }
113
114    #[tokio::test]
115    async fn tempo_fork_schedule_rejects_unknown_hardfork() {
116        let asserter = Asserter::new();
117        asserter.push_success(&serde_json::json!({ "active": "FutureFork", "schedule": [] }));
118        let provider = mocked_provider(asserter);
119        assert!(is_tempo_hardfork_active(&provider, TempoHardfork::T3).await.is_err());
120    }
121
122    #[test]
123    fn hardfork_from_anvil_node_info_requires_tempo_network() {
124        let info = |network: &str, hard_fork: &str| AnvilNodeInfo {
125            network: Some(network.to_string()),
126            hard_fork: Some(hard_fork.to_string()),
127        };
128        assert_eq!(hardfork_from_anvil_node_info(&info("tempo", "T3")), Some(TempoHardfork::T3));
129        assert_eq!(hardfork_from_anvil_node_info(&info("tempo", "T11")), Some(TempoHardfork::T11));
130        assert_eq!(hardfork_from_anvil_node_info(&info("tempo", "FutureFork")), None);
131        assert_eq!(hardfork_from_anvil_node_info(&info("ethereum", "T3")), None);
132    }
133
134    #[tokio::test]
135    async fn anvil_node_info_fallback_detects_hardfork_activation() {
136        let asserter = Asserter::new();
137        for _ in 0..2 {
138            asserter.push_failure(ErrorPayload {
139                code: -32601,
140                message: "Method not found".into(),
141                data: None,
142            });
143            asserter.push_success(&serde_json::json!({ "network": "tempo", "hardFork": "T3" }));
144        }
145        let provider =
146            ProviderBuilder::new().network::<TempoNetwork>().connect_mocked_client(asserter);
147        assert!(is_tempo_hardfork_active(&provider, TempoHardfork::T3).await.unwrap());
148        assert!(!is_tempo_hardfork_active(&provider, TempoHardfork::T4).await.unwrap());
149    }
150
151    #[tokio::test]
152    async fn expiring_nonce_discriminator_requires_t12_when_hardfork_is_known() {
153        let request = |nonce_key, nonce| {
154            let mut tx = TempoTransactionRequest::default();
155            tx.set_nonce_key(nonce_key);
156            tx.set_nonce(nonce);
157            tx
158        };
159        let discriminator = request(TEMPO_EXPIRING_NONCE_KEY, 7);
160
161        // `None` leaves the mock without a response, so any hardfork query fails.
162        for (tx, active, accepted) in [
163            (&discriminator, Some("T11"), false),
164            (&discriminator, Some("T12"), true),
165            (&discriminator, Some("FutureFork"), true),
166            (&discriminator, None, true),
167            (&request(TEMPO_EXPIRING_NONCE_KEY, 0), None, true),
168            (&request(U256::from(1), 7), None, true),
169        ] {
170            let asserter = Asserter::new();
171            if let Some(active) = active {
172                asserter.push_success(&serde_json::json!({ "active": active, "schedule": [] }));
173            }
174            let provider = mocked_provider(asserter);
175            let result = ensure_expiring_nonce_discriminator_active(&provider, tx).await;
176            assert_eq!(
177                result.is_ok(),
178                accepted,
179                "nonce {:?} at {active:?}: {result:?}",
180                tx.nonce()
181            );
182        }
183    }
184}