zebra_rpc/methods/types/
long_poll.rs

1//! Long polling support for the `getblocktemplate` RPC.
2//!
3//! These implementation details are private, and should not be relied upon by miners.
4//! They are also different from the `zcashd` implementation of long polling.
5
6use std::{str::FromStr, sync::Arc};
7
8use serde::{Deserialize, Serialize};
9
10use zebra_chain::{
11    block::{self, Height},
12    serialization::DateTime32,
13    transaction::{self, UnminedTxId},
14};
15use zebra_node_services::BoxError;
16
17/// The length of a serialized [`LongPollId`] string.
18///
19/// This is an internal Zebra implementation detail, which does not need to match `zcashd`.
20pub const LONG_POLL_ID_LENGTH: usize = 46;
21
22/// The inputs to the long polling check.
23///
24/// If these inputs change, Zebra should return a response to any open long polls.
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct LongPollInput {
27    // Fields that invalidate old work:
28    //
29    /// The tip height used to generate the template containing this long poll ID.
30    ///
31    /// If the tip block height changes, a new template must be provided.
32    /// Old work is no longer valid.
33    ///
34    /// The height is technically redundant, but it helps with debugging.
35    /// It also reduces the probability of a missed tip change.
36    pub tip_height: Height,
37
38    /// The tip hash used to generate the template containing this long poll ID.
39    ///
40    /// If the tip block changes, a new template must be provided.
41    /// Old work is no longer valid.
42    pub tip_hash: block::Hash,
43
44    /// The max time in the same template as this long poll ID.
45    ///
46    /// If the max time is reached, a new template must be provided.
47    /// Old work is no longer valid.
48    ///
49    /// Ideally, a new template should be provided at least one target block interval before
50    /// the max time. This avoids wasted work.
51    pub max_time: DateTime32,
52
53    // Fields that allow old work:
54    //
55    /// The effecting hashes of the transactions in the mempool,
56    /// when the template containing this long poll ID was generated.
57    /// We ignore changes to authorizing data.
58    ///
59    /// This might be different from the transactions in the template, due to ZIP-317.
60    ///
61    /// If the mempool transactions change, a new template might be provided.
62    /// Old work is still valid.
63    pub mempool_transaction_mined_ids: Arc<[transaction::Hash]>,
64}
65
66impl LongPollInput {
67    /// Returns a new [`LongPollInput`], based on the supplied fields.
68    pub fn new(
69        tip_height: Height,
70        tip_hash: block::Hash,
71        max_time: DateTime32,
72        mempool_tx_ids: impl IntoIterator<Item = UnminedTxId>,
73    ) -> Self {
74        let mut tx_mined_ids: Vec<transaction::Hash> =
75            mempool_tx_ids.into_iter().map(|id| id.mined_id()).collect();
76
77        // The mempool returns unordered transactions, we need to sort them here so
78        // that the longpollid doesn't change unexpectedly.
79        tx_mined_ids.sort();
80
81        LongPollInput {
82            tip_height,
83            tip_hash,
84            max_time,
85            mempool_transaction_mined_ids: tx_mined_ids.into(),
86        }
87    }
88
89    /// Returns the [`LongPollId`] for this [`LongPollInput`].
90    /// Performs lossy conversion on some fields.
91    pub fn generate_id(&self) -> LongPollId {
92        let mut tip_hash_checksum = 0;
93        update_checksum(&mut tip_hash_checksum, self.tip_hash.0);
94
95        let mut mempool_transaction_content_checksum: u32 = 0;
96        for tx_mined_id in self.mempool_transaction_mined_ids.iter() {
97            update_checksum(&mut mempool_transaction_content_checksum, tx_mined_id.0);
98        }
99
100        LongPollId {
101            tip_height: self.tip_height.0,
102
103            tip_hash_checksum,
104
105            max_timestamp: self.max_time.timestamp(),
106
107            // It's ok to do wrapping conversions here,
108            // because long polling checks are probabilistic.
109            mempool_transaction_count: self.mempool_transaction_mined_ids.len() as u32,
110
111            mempool_transaction_content_checksum,
112        }
113    }
114}
115
116/// The encoded long poll ID, generated from the [`LongPollInput`].
117///
118/// `zcashd` IDs are currently 69 hex/decimal digits long.
119/// Since Zebra's IDs are only 46 hex/decimal digits, mining pools should be able to handle them.
120#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
121#[serde(try_from = "String", into = "String")]
122pub struct LongPollId {
123    // Fields that invalidate old work:
124    //
125    /// The tip height used to generate the template containing this long poll ID.
126    ///
127    /// If the tip block height changes, a new template must be provided.
128    /// Old work is no longer valid.
129    ///
130    /// The height is technically redundant, but it helps with debugging.
131    /// It also reduces the probability of a missed tip change.
132    pub tip_height: u32,
133
134    /// A checksum of the tip hash used to generate the template containing this long poll ID.
135    ///
136    /// If the tip block changes, a new template must be provided.
137    /// Old work is no longer valid.
138    /// This checksum is not cryptographically secure.
139    ///
140    /// It's ok to do a probabilistic check here,
141    /// so we choose a 1 in 2^32 chance of missing a block change.
142    pub tip_hash_checksum: u32,
143
144    /// The max time in the same template as this long poll ID.
145    ///
146    /// If the max time is reached, a new template must be provided.
147    /// Old work is no longer valid.
148    ///
149    /// Ideally, a new template should be provided at least one target block interval before
150    /// the max time. This avoids wasted work.
151    ///
152    /// Zcash times are limited to 32 bits by the consensus rules.
153    pub max_timestamp: u32,
154
155    // Fields that allow old work:
156    //
157    /// The number of transactions in the mempool when the template containing this long poll ID
158    /// was generated. This might be different from the number of transactions in the template,
159    /// due to ZIP-317.
160    ///
161    /// If the number of mempool transactions changes, a new template might be provided.
162    /// Old work is still valid.
163    ///
164    /// The number of transactions is limited by the mempool DoS limit.
165    ///
166    /// Using the number of transactions makes mempool checksum attacks much harder.
167    /// It also helps with debugging, and reduces the probability of a missed mempool change.
168    pub mempool_transaction_count: u32,
169
170    /// A checksum of the effecting hashes of the transactions in the mempool,
171    /// when the template containing this long poll ID was generated.
172    /// We ignore changes to authorizing data.
173    ///
174    /// This might be different from the transactions in the template, due to ZIP-317.
175    ///
176    /// If the content of the mempool changes, a new template might be provided.
177    /// Old work is still valid.
178    ///
179    /// This checksum is not cryptographically secure.
180    ///
181    /// It's ok to do a probabilistic check here,
182    /// so we choose a 1 in 2^32 chance of missing a transaction change.
183    ///
184    /// # Security
185    ///
186    /// Attackers could use dust transactions to keep the checksum at the same value.
187    /// But this would likely change the number of transactions in the mempool.
188    ///
189    /// If an attacker could also keep the number of transactions constant,
190    /// a new template will be generated when the tip hash changes, or the max time is reached.
191    pub mempool_transaction_content_checksum: u32,
192}
193
194impl LongPollId {
195    /// Returns `true` if shares using `old_long_poll_id` can be submitted in response to the
196    /// template for `self`:
197    /// <https://en.bitcoin.it/wiki/BIP_0022#Optional:_Long_Polling>
198    ///
199    /// Old shares may be valid if only the mempool transactions have changed,
200    /// because newer transactions don't have to be included in the old shares.
201    ///
202    /// But if the chain tip has changed, the block header has changed, so old shares are invalid.
203    /// (And if the max time has changed on testnet, the block header has changed.)
204    pub fn submit_old(&self, old_long_poll_id: &LongPollId) -> bool {
205        self.tip_height == old_long_poll_id.tip_height
206            && self.tip_hash_checksum == old_long_poll_id.tip_hash_checksum
207            && self.max_timestamp == old_long_poll_id.max_timestamp
208    }
209}
210
211/// Update `checksum` from `item`, so changes in `item` are likely to also change `checksum`.
212///
213/// This checksum is not cryptographically secure.
214fn update_checksum(checksum: &mut u32, item: [u8; 32]) {
215    for chunk in item.chunks(4) {
216        let chunk = chunk.try_into().expect("chunk is u32 size");
217
218        // The endianness of this conversion doesn't matter,
219        // so we make it efficient on the most common platforms.
220        let chunk = u32::from_le_bytes(chunk);
221
222        // It's ok to use xor here, because long polling checks are probabilistic,
223        // and the height, time, and transaction count fields will detect most changes.
224        //
225        // Without those fields, miners could game the xor-ed block hash,
226        // and hide block changes from other miners, gaining an advantage.
227        // But this would reduce their profit under proof of work,
228        // because the first valid block hash a miner generates will pay
229        // a significant block subsidy.
230        *checksum ^= chunk;
231    }
232}
233
234impl std::fmt::Display for LongPollId {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        let LongPollId {
237            tip_height,
238            tip_hash_checksum,
239            max_timestamp,
240            mempool_transaction_count,
241            mempool_transaction_content_checksum,
242        } = self;
243
244        // We can't do this using `serde`, because it names each field,
245        // but we want a single string containing all the fields.
246        write!(
247            f,
248            // Height as decimal, padded with zeroes to the width of Height::MAX
249            // Checksums as hex, padded with zeroes to the width of u32::MAX
250            // Timestamp as decimal, padded with zeroes to the width of u32::MAX
251            // Transaction Count as decimal, padded with zeroes to the width of u32::MAX
252            "{tip_height:010}\
253             {tip_hash_checksum:08x}\
254             {max_timestamp:010}\
255             {mempool_transaction_count:010}\
256             {mempool_transaction_content_checksum:08x}"
257        )
258    }
259}
260
261impl FromStr for LongPollId {
262    type Err = BoxError;
263
264    /// Exact conversion from a string to LongPollId.
265    fn from_str(long_poll_id: &str) -> Result<Self, Self::Err> {
266        if long_poll_id.len() != LONG_POLL_ID_LENGTH {
267            return Err(format!(
268                "incorrect long poll id length, must be {LONG_POLL_ID_LENGTH} for Zebra"
269            )
270            .into());
271        }
272
273        Ok(Self {
274            tip_height: long_poll_id[0..10].parse()?,
275            tip_hash_checksum: u32::from_str_radix(&long_poll_id[10..18], 16)?,
276            max_timestamp: long_poll_id[18..28].parse()?,
277            mempool_transaction_count: long_poll_id[28..38].parse()?,
278            mempool_transaction_content_checksum: u32::from_str_radix(
279                &long_poll_id[38..LONG_POLL_ID_LENGTH],
280                16,
281            )?,
282        })
283    }
284}
285
286// Wrappers for serde conversion
287impl From<LongPollId> for String {
288    fn from(id: LongPollId) -> Self {
289        id.to_string()
290    }
291}
292
293impl TryFrom<String> for LongPollId {
294    type Error = BoxError;
295
296    fn try_from(s: String) -> Result<Self, Self::Error> {
297        s.parse()
298    }
299}
300
301/// Check that [`LongPollInput::new`] will sort mempool transaction ids.
302///
303/// The mempool does not currently guarantee the order in which it will return transactions and
304/// may return the same items in a different order, while the long poll id should be the same if
305/// its other components are equal and no transactions have been added or removed in the mempool.
306#[test]
307fn long_poll_input_mempool_tx_ids_are_sorted() {
308    let mempool_tx_ids = || {
309        (0..10)
310            .map(|i| transaction::Hash::from([i; 32]))
311            .map(UnminedTxId::Legacy)
312    };
313
314    assert_eq!(
315        LongPollInput::new(Height::MIN, Default::default(), 0.into(), mempool_tx_ids()),
316        LongPollInput::new(
317            Height::MIN,
318            Default::default(),
319            0.into(),
320            mempool_tx_ids().rev()
321        ),
322        "long poll input should sort mempool tx ids"
323    );
324}