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