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(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Getters, new)]
123#[serde(try_from = "String", into = "String")]
124pub struct LongPollId {
125 // Fields that invalidate old work:
126 //
127 /// The tip height used to generate the template containing this long poll ID.
128 ///
129 /// If the tip block height changes, a new template must be provided.
130 /// Old work is no longer valid.
131 ///
132 /// The height is technically redundant, but it helps with debugging.
133 /// It also reduces the probability of a missed tip change.
134 pub(crate) tip_height: u32,
135
136 /// A checksum of the tip hash used to generate the template containing this long poll ID.
137 ///
138 /// If the tip block changes, a new template must be provided.
139 /// Old work is no longer valid.
140 /// This checksum is not cryptographically secure.
141 ///
142 /// It's ok to do a probabilistic check here,
143 /// so we choose a 1 in 2^32 chance of missing a block change.
144 pub(crate) tip_hash_checksum: u32,
145
146 /// The max time in the same template as this long poll ID.
147 ///
148 /// If the max time is reached, a new template must be provided.
149 /// Old work is no longer valid.
150 ///
151 /// Ideally, a new template should be provided at least one target block interval before
152 /// the max time. This avoids wasted work.
153 ///
154 /// Zcash times are limited to 32 bits by the consensus rules.
155 pub(crate) max_timestamp: u32,
156
157 // Fields that allow old work:
158 //
159 /// The number of transactions in the mempool when the template containing this long poll ID
160 /// was generated. This might be different from the number of transactions in the template,
161 /// due to ZIP-317.
162 ///
163 /// If the number of mempool transactions changes, a new template might be provided.
164 /// Old work is still valid.
165 ///
166 /// The number of transactions is limited by the mempool DoS limit.
167 ///
168 /// Using the number of transactions makes mempool checksum attacks much harder.
169 /// It also helps with debugging, and reduces the probability of a missed mempool change.
170 pub(crate) mempool_transaction_count: u32,
171
172 /// A checksum of the effecting hashes of the transactions in the mempool,
173 /// when the template containing this long poll ID was generated.
174 /// We ignore changes to authorizing data.
175 ///
176 /// This might be different from the transactions in the template, due to ZIP-317.
177 ///
178 /// If the content of the mempool changes, a new template might be provided.
179 /// Old work is still valid.
180 ///
181 /// This checksum is not cryptographically secure.
182 ///
183 /// It's ok to do a probabilistic check here,
184 /// so we choose a 1 in 2^32 chance of missing a transaction change.
185 ///
186 /// # Security
187 ///
188 /// Attackers could use dust transactions to keep the checksum at the same value.
189 /// But this would likely change the number of transactions in the mempool.
190 ///
191 /// If an attacker could also keep the number of transactions constant,
192 /// a new template will be generated when the tip hash changes, or the max time is reached.
193 pub(crate) mempool_transaction_content_checksum: u32,
194}
195
196impl LongPollId {
197 /// Returns `true` if shares using `old_long_poll_id` can be submitted in response to the
198 /// template for `self`:
199 /// <https://en.bitcoin.it/wiki/BIP_0022#Optional:_Long_Polling>
200 ///
201 /// Old shares may be valid if only the mempool transactions have changed,
202 /// because newer transactions don't have to be included in the old shares.
203 ///
204 /// But if the chain tip has changed, the block header has changed, so old shares are invalid.
205 /// (And if the max time has changed on testnet, the block header has changed.)
206 pub fn submit_old(&self, old_long_poll_id: &LongPollId) -> bool {
207 self.tip_height == old_long_poll_id.tip_height
208 && self.tip_hash_checksum == old_long_poll_id.tip_hash_checksum
209 && self.max_timestamp == old_long_poll_id.max_timestamp
210 }
211}
212
213/// Update `checksum` from `item`, so changes in `item` are likely to also change `checksum`.
214///
215/// This checksum is not cryptographically secure.
216fn update_checksum(checksum: &mut u32, item: [u8; 32]) {
217 for chunk in item.chunks(4) {
218 let chunk = chunk.try_into().expect("chunk is u32 size");
219
220 // The endianness of this conversion doesn't matter,
221 // so we make it efficient on the most common platforms.
222 let chunk = u32::from_le_bytes(chunk);
223
224 // It's ok to use xor here, because long polling checks are probabilistic,
225 // and the height, time, and transaction count fields will detect most changes.
226 //
227 // Without those fields, miners could game the xor-ed block hash,
228 // and hide block changes from other miners, gaining an advantage.
229 // But this would reduce their profit under proof of work,
230 // because the first valid block hash a miner generates will pay
231 // a significant block subsidy.
232 *checksum ^= chunk;
233 }
234}
235
236impl std::fmt::Display for LongPollId {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 let LongPollId {
239 tip_height,
240 tip_hash_checksum,
241 max_timestamp,
242 mempool_transaction_count,
243 mempool_transaction_content_checksum,
244 } = self;
245
246 // We can't do this using `serde`, because it names each field,
247 // but we want a single string containing all the fields.
248 write!(
249 f,
250 // Height as decimal, padded with zeroes to the width of Height::MAX
251 // Checksums as hex, padded with zeroes to the width of u32::MAX
252 // Timestamp as decimal, padded with zeroes to the width of u32::MAX
253 // Transaction Count as decimal, padded with zeroes to the width of u32::MAX
254 "{tip_height:010}\
255 {tip_hash_checksum:08x}\
256 {max_timestamp:010}\
257 {mempool_transaction_count:010}\
258 {mempool_transaction_content_checksum:08x}"
259 )
260 }
261}
262
263impl FromStr for LongPollId {
264 type Err = BoxError;
265
266 /// Exact conversion from a string to LongPollId.
267 fn from_str(long_poll_id: &str) -> Result<Self, Self::Err> {
268 if long_poll_id.len() != LONG_POLL_ID_LENGTH {
269 return Err(format!(
270 "incorrect long poll id length, must be {LONG_POLL_ID_LENGTH} for Zebra"
271 )
272 .into());
273 }
274
275 Ok(Self {
276 tip_height: long_poll_id[0..10].parse()?,
277 tip_hash_checksum: u32::from_str_radix(&long_poll_id[10..18], 16)?,
278 max_timestamp: long_poll_id[18..28].parse()?,
279 mempool_transaction_count: long_poll_id[28..38].parse()?,
280 mempool_transaction_content_checksum: u32::from_str_radix(
281 &long_poll_id[38..LONG_POLL_ID_LENGTH],
282 16,
283 )?,
284 })
285 }
286}
287
288// Wrappers for serde conversion
289impl From<LongPollId> for String {
290 fn from(id: LongPollId) -> Self {
291 id.to_string()
292 }
293}
294
295impl TryFrom<String> for LongPollId {
296 type Error = BoxError;
297
298 fn try_from(s: String) -> Result<Self, Self::Error> {
299 s.parse()
300 }
301}
302
303/// Check that [`LongPollInput::new`] will sort mempool transaction ids.
304///
305/// The mempool does not currently guarantee the order in which it will return transactions and
306/// may return the same items in a different order, while the long poll id should be the same if
307/// its other components are equal and no transactions have been added or removed in the mempool.
308#[test]
309fn long_poll_input_mempool_tx_ids_are_sorted() {
310 let mempool_tx_ids = || {
311 (0..10)
312 .map(|i| transaction::Hash::from([i; 32]))
313 .map(UnminedTxId::Legacy)
314 };
315
316 assert_eq!(
317 LongPollInput::new(Height::MIN, Default::default(), 0.into(), mempool_tx_ids()),
318 LongPollInput::new(
319 Height::MIN,
320 Default::default(),
321 0.into(),
322 mempool_tx_ids().rev()
323 ),
324 "long poll input should sort mempool tx ids"
325 );
326}