zebra_rpc/methods/types/get_block_template/
proposal.rs

1//! getblocktemplate proposal mode implementation.
2//!
3//! `BlockProposalResponse` is the output of the `getblocktemplate` RPC method in 'proposal' mode.
4
5use std::{num::ParseIntError, str::FromStr, sync::Arc};
6
7use zebra_chain::{
8    block::{self, Block, Height},
9    parameters::{Network, NetworkUpgrade},
10    serialization::{DateTime32, SerializationError, ZcashDeserializeInto},
11    work::equihash::Solution,
12};
13use zebra_node_services::BoxError;
14
15use crate::methods::types::{
16    default_roots::DefaultRoots,
17    get_block_template::{BlockTemplateResponse, GetBlockTemplateResponse},
18};
19
20/// Response to a `getblocktemplate` RPC request in proposal mode.
21///
22/// <https://en.bitcoin.it/wiki/BIP_0022#Appendix:_Example_Rejection_Reasons>
23///
24/// Note:
25/// The error response specification at <https://en.bitcoin.it/wiki/BIP_0023#Block_Proposal>
26/// seems to have a copy-paste issue, or it is under-specified. We follow the `zcashd`
27/// implementation instead, which returns a single raw string.
28#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29#[serde(untagged, rename_all = "kebab-case")]
30pub enum BlockProposalResponse {
31    /// Block proposal was rejected as invalid.
32    /// Contains the reason that the proposal was invalid.
33    ///
34    /// TODO: turn this into a typed error enum?
35    Rejected(String),
36
37    /// Block proposal was successfully validated, returns null.
38    Valid,
39}
40
41impl BlockProposalResponse {
42    /// Return a rejected response containing an error kind and detailed error info.
43    ///
44    /// Note: Error kind is currently ignored to match zcashd error format (`kebab-case` string).
45    pub fn rejected<S: ToString>(_kind: S, error: BoxError) -> Self {
46        // Make error `kebab-case` to match zcashd format.
47        let error_kebab1 = format!("{error:?}")
48            .replace(|c: char| !c.is_alphanumeric(), "-")
49            .to_ascii_lowercase();
50        // Remove consecutive duplicated `-` characters.
51        let mut error_v: Vec<char> = error_kebab1.chars().collect();
52        error_v.dedup_by(|a, b| a == &'-' && b == &'-');
53        let error_kebab2: String = error_v.into_iter().collect();
54        // Trim any leading or trailing `-` characters.
55        let final_error = error_kebab2.trim_matches('-');
56
57        BlockProposalResponse::Rejected(final_error.to_string())
58    }
59
60    /// Returns true if self is [`BlockProposalResponse::Valid`]
61    pub fn is_valid(&self) -> bool {
62        matches!(self, Self::Valid)
63    }
64}
65
66impl From<BlockProposalResponse> for GetBlockTemplateResponse {
67    fn from(proposal_response: BlockProposalResponse) -> Self {
68        Self::ProposalMode(proposal_response)
69    }
70}
71
72impl From<BlockTemplateResponse> for GetBlockTemplateResponse {
73    fn from(template: BlockTemplateResponse) -> Self {
74        Self::TemplateMode(Box::new(template))
75    }
76}
77
78/// The source of the time in the block proposal header.
79#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
80pub enum BlockTemplateTimeSource {
81    /// The `curtime` field in the template.
82    /// This is the default time source.
83    #[default]
84    CurTime,
85
86    /// The `mintime` field in the template.
87    MinTime,
88
89    /// The `maxtime` field in the template.
90    MaxTime,
91
92    /// The supplied time, clamped within the template's `[mintime, maxtime]`.
93    Clamped(DateTime32),
94
95    /// The current local clock time, clamped within the template's `[mintime, maxtime]`.
96    ClampedNow,
97
98    /// The raw supplied time, ignoring the `mintime` and `maxtime` in the template.
99    ///
100    /// Warning: this can create an invalid block proposal.
101    Raw(DateTime32),
102
103    /// The raw current local time, ignoring the `mintime` and `maxtime` in the template.
104    ///
105    /// Warning: this can create an invalid block proposal.
106    RawNow,
107}
108
109impl BlockTemplateTimeSource {
110    /// Returns the time from `template` using this time source.
111    pub fn time_from_template(&self, template: &BlockTemplateResponse) -> DateTime32 {
112        use BlockTemplateTimeSource::*;
113
114        match self {
115            CurTime => template.cur_time,
116            MinTime => template.min_time,
117            MaxTime => template.max_time,
118            Clamped(time) => (*time).clamp(template.min_time, template.max_time),
119            ClampedNow => DateTime32::now().clamp(template.min_time, template.max_time),
120            Raw(time) => *time,
121            RawNow => DateTime32::now(),
122        }
123    }
124
125    /// Returns true if this time source uses `max_time` in any way, including clamping.
126    pub fn uses_max_time(&self) -> bool {
127        use BlockTemplateTimeSource::*;
128
129        match self {
130            CurTime | MinTime => false,
131            MaxTime | Clamped(_) | ClampedNow => true,
132            Raw(_) | RawNow => false,
133        }
134    }
135
136    /// Returns an iterator of time sources that are valid according to the consensus rules.
137    pub fn valid_sources() -> impl IntoIterator<Item = BlockTemplateTimeSource> {
138        use BlockTemplateTimeSource::*;
139
140        [CurTime, MinTime, MaxTime, ClampedNow].into_iter()
141    }
142}
143
144impl FromStr for BlockTemplateTimeSource {
145    type Err = ParseIntError;
146
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        use BlockTemplateTimeSource::*;
149
150        match s.to_lowercase().as_str() {
151            "curtime" => Ok(CurTime),
152            "mintime" => Ok(MinTime),
153            "maxtime" => Ok(MaxTime),
154            "clampednow" => Ok(ClampedNow),
155            "rawnow" => Ok(RawNow),
156            s => match s.strip_prefix("raw") {
157                // "raw"u32
158                Some(raw_value) => Ok(Raw(raw_value.parse()?)),
159                // "clamped"u32 or just u32
160                // this is the default if the argument is just a number
161                None => Ok(Clamped(s.strip_prefix("clamped").unwrap_or(s).parse()?)),
162            },
163        }
164    }
165}
166
167/// Returns a block proposal generated from a [`BlockTemplateResponse`] RPC response.
168///
169/// If `time_source` is not supplied, uses the current time from the template.
170pub fn proposal_block_from_template(
171    template: &BlockTemplateResponse,
172    time_source: impl Into<Option<BlockTemplateTimeSource>>,
173    net: &Network,
174) -> Result<Block, SerializationError> {
175    let BlockTemplateResponse {
176        version,
177        height,
178        previous_block_hash,
179        default_roots:
180            DefaultRoots {
181                merkle_root,
182                block_commitments_hash,
183                chain_history_root,
184                ..
185            },
186        bits: difficulty_threshold,
187        ref coinbase_txn,
188        transactions: ref tx_templates,
189        ..
190    } = *template;
191
192    let height = Height(height);
193
194    // TODO: Refactor [`Height`] so that these checks lose relevance.
195    if height > Height::MAX {
196        Err(SerializationError::Parse(
197            "height of coinbase transaction is {height}, which exceeds the maximum of {Height::MAX}",
198        ))?;
199    };
200
201    let time = time_source
202        .into()
203        .unwrap_or_default()
204        .time_from_template(template)
205        .into();
206
207    let mut transactions = vec![coinbase_txn.data.as_ref().zcash_deserialize_into()?];
208
209    for tx_template in tx_templates {
210        transactions.push(tx_template.data.as_ref().zcash_deserialize_into()?);
211    }
212
213    let commitment_bytes = match NetworkUpgrade::current(net, height) {
214        NetworkUpgrade::Canopy => chain_history_root.bytes_in_serialized_order(),
215        NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 | NetworkUpgrade::Nu7 => {
216            block_commitments_hash.bytes_in_serialized_order()
217        }
218        _ => Err(SerializationError::Parse(
219            "Zebra does not support generating pre-Canopy block templates",
220        ))?,
221    }
222    .into();
223
224    Ok(Block {
225        header: Arc::new(block::Header {
226            version,
227            previous_block_hash,
228            merkle_root,
229            commitment_bytes,
230            time,
231            difficulty_threshold,
232            nonce: [0; 32].into(),
233            solution: Solution::for_proposal(),
234        }),
235        transactions,
236    })
237}