Skip to main content

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