• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ergoplatform / sigma-rust / 13994202105

21 Mar 2025 02:37PM UTC coverage: 78.402% (+0.07%) from 78.333%
13994202105

Pull #801

github

web-flow
Merge aaf043c2f into b5bdb7089
Pull Request #801: Add Header.checkPow

37 of 43 new or added lines in 7 files covered. (86.05%)

10 existing lines in 1 file now uncovered.

11257 of 14358 relevant lines covered (78.4%)

6.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.29
/ergo-chain-types/src/header.rs
1
//! Block header
2
use crate::autolykos_pow_scheme::{
3
    decode_compact_bits, order_bigint, AutolykosPowScheme, AutolykosPowSchemeError,
4
};
5
use crate::{ADDigest, BlockId, Digest32, EcPoint};
6
use alloc::boxed::Box;
7
use alloc::vec::Vec;
8
use core2::io::Write;
9
use num_bigint::BigInt;
10
use sigma_ser::vlq_encode::{ReadSigmaVlqExt, WriteSigmaVlqExt};
11
use sigma_ser::{
12
    ScorexParsingError, ScorexSerializable, ScorexSerializationError, ScorexSerializeResult,
13
};
14
use sigma_util::hash::blake2b256_hash;
15

16
use crate::votes::Votes;
17

18
/// Represents data of the block header available in Sigma propositions.
19
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
20
#[derive(PartialEq, Eq, Debug, Clone)]
21
pub struct Header {
22
    /// Block version, to be increased on every soft and hardfork.
23
    #[cfg_attr(feature = "json", serde(rename = "version"))]
24
    pub version: u8,
25
    /// Bytes representation of ModifierId of this Header
26
    #[cfg_attr(feature = "json", serde(rename = "id"))]
27
    pub id: BlockId,
28
    /// Bytes representation of ModifierId of the parent block
29
    #[cfg_attr(feature = "json", serde(rename = "parentId"))]
30
    pub parent_id: BlockId,
31
    /// Hash of ADProofs for transactions in a block
32
    #[cfg_attr(feature = "json", serde(rename = "adProofsRoot"))]
33
    pub ad_proofs_root: Digest32,
34
    /// AvlTree of a state after block application
35
    #[cfg_attr(feature = "json", serde(rename = "stateRoot"))]
36
    pub state_root: ADDigest,
37
    /// Root hash (for a Merkle tree) of transactions in a block.
38
    #[cfg_attr(feature = "json", serde(rename = "transactionsRoot"))]
39
    pub transaction_root: Digest32,
40
    /// Timestamp of a block in ms from UNIX epoch
41
    #[cfg_attr(feature = "json", serde(rename = "timestamp"))]
42
    pub timestamp: u64,
43
    /// Current difficulty in a compressed view.
44
    #[cfg_attr(feature = "json", serde(rename = "nBits"))]
45
    pub n_bits: u64,
46
    /// Block height
47
    #[cfg_attr(feature = "json", serde(rename = "height"))]
48
    pub height: u32,
49
    /// Root hash of extension section
50
    #[cfg_attr(feature = "json", serde(rename = "extensionHash"))]
51
    pub extension_root: Digest32,
52
    /// Solution for an Autolykos PoW puzzle
53
    #[cfg_attr(feature = "json", serde(rename = "powSolutions"))]
54
    pub autolykos_solution: AutolykosSolution,
55
    /// Miner votes for changing system parameters.
56
    /// 3 bytes in accordance to Scala implementation, but will use `Vec` until further improvements
57
    #[cfg_attr(feature = "json", serde(rename = "votes"))]
58
    pub votes: Votes,
59
}
60

61
impl Header {
62
    /// Used in nipowpow
63
    pub fn serialize_without_pow(&self) -> Result<Vec<u8>, ScorexSerializationError> {
19✔
64
        let mut data = Vec::new();
19✔
65
        let mut w = &mut data;
16✔
66
        w.put_u8(self.version)?;
35✔
67
        self.parent_id.0.scorex_serialize(&mut w)?;
40✔
68
        self.ad_proofs_root.scorex_serialize(&mut w)?;
37✔
69
        self.transaction_root.scorex_serialize(&mut w)?;
37✔
70
        self.state_root.scorex_serialize(&mut w)?;
35✔
71
        w.put_u64(self.timestamp)?;
38✔
72
        self.extension_root.scorex_serialize(&mut w)?;
36✔
73

74
        // n_bits needs to be serialized in big-endian format. Note that it actually fits in a
75
        // `u32`.
76
        let n_bits_be = (self.n_bits as u32).to_be_bytes();
36✔
77
        w.write_all(&n_bits_be)?;
20✔
78

79
        w.put_u32(self.height)?;
36✔
80
        w.write_all(&self.votes.0)?;
36✔
81

82
        // For block version >= 2, this new byte encodes length of possible new fields.
83
        // Set to 0 for now, so no new fields.
84
        if self.version > 1 {
16✔
85
            w.put_i8(0)?;
28✔
86
        }
87
        Ok(data)
16✔
88
    }
89
    /// Check that proof of work was valid for header. Only Autolykos2 is supported
90
    pub fn check_pow(&self) -> Result<bool, AutolykosPowSchemeError> {
4✔
91
        if self.version != 1 {
4✔
92
            let hit = AutolykosPowScheme::default().pow_hit(self)?;
8✔
93
            let target = order_bigint() / decode_compact_bits(self.n_bits);
8✔
94
            Ok(hit < target)
8✔
95
        } else {
NEW
96
            Err(AutolykosPowSchemeError::Unsupported)
×
97
        }
98
    }
99
}
100

101
impl ScorexSerializable for Header {
102
    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, w: &mut W) -> ScorexSerializeResult {
4✔
103
        let bytes = self.serialize_without_pow()?;
4✔
104
        w.write_all(&bytes)?;
8✔
105

106
        // Serialize `AutolykosSolution`
107
        self.autolykos_solution.serialize_bytes(self.version, w)?;
12✔
108
        Ok(())
4✔
109
    }
110

111
    fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
4✔
112
        let version = r.get_u8()?;
4✔
113
        let parent_id = BlockId(Digest32::scorex_parse(r)?);
8✔
114
        let ad_proofs_root = Digest32::scorex_parse(r)?;
8✔
115
        let transaction_root = Digest32::scorex_parse(r)?;
8✔
116
        let state_root = ADDigest::scorex_parse(r)?;
8✔
117
        let timestamp = r.get_u64()?;
8✔
118
        let extension_root = Digest32::scorex_parse(r)?;
8✔
119
        let mut n_bits_buf = [0u8, 0, 0, 0];
4✔
120
        r.read_exact(&mut n_bits_buf)?;
4✔
121
        let n_bits = u32::from_be_bytes(n_bits_buf) as u64;
4✔
122
        let height = r.get_u32()?;
8✔
123
        let mut votes_bytes = [0u8, 0, 0];
4✔
124
        r.read_exact(&mut votes_bytes)?;
4✔
125
        let votes = Votes(votes_bytes);
4✔
126

127
        // For block version >= 2, a new byte encodes length of possible new fields.  If this byte >
128
        // 0, we read new fields but do nothing, as semantics of the fields is not known.
129
        if version > 1 {
4✔
130
            let new_field_size = r.get_u8()?;
2✔
131
            if new_field_size > 0 {
2✔
132
                let mut field_bytes: Vec<u8> = core::iter::repeat(0)
×
133
                    .take(new_field_size as usize)
×
134
                    .collect();
135
                r.read_exact(&mut field_bytes)?;
×
136
            }
137
        }
138

139
        // Parse `AutolykosSolution`
140
        let autolykos_solution = if version == 1 {
6✔
141
            let miner_pk = EcPoint::scorex_parse(r)?.into();
8✔
142
            let pow_onetime_pk = Some(EcPoint::scorex_parse(r)?.into());
8✔
143
            let mut nonce: Vec<u8> = core::iter::repeat(0).take(8).collect();
8✔
144
            r.read_exact(&mut nonce)?;
8✔
145
            let d_bytes_len = r.get_u8()?;
8✔
146
            let mut d_bytes: Vec<u8> = core::iter::repeat(0).take(d_bytes_len as usize).collect();
8✔
147
            r.read_exact(&mut d_bytes)?;
8✔
148
            let pow_distance = Some(BigInt::from_signed_bytes_be(&d_bytes));
8✔
149
            AutolykosSolution {
150
                miner_pk,
151
                pow_onetime_pk,
152
                nonce,
153
                pow_distance,
154
            }
155
        } else {
156
            // autolykos v2
157
            let pow_onetime_pk = None;
2✔
158
            let pow_distance = None;
2✔
159
            let miner_pk = EcPoint::scorex_parse(r)?.into();
4✔
160
            let mut nonce: Vec<u8> = core::iter::repeat(0).take(8).collect();
4✔
161
            r.read_exact(&mut nonce)?;
4✔
162
            AutolykosSolution {
163
                miner_pk,
164
                pow_onetime_pk,
165
                nonce,
166
                pow_distance,
167
            }
168
        };
169

170
        // The `Header.id` field isn't serialized/deserialized but rather computed as a hash of
171
        // every other field in `Header`. First we initialize header with dummy id field then
172
        // compute the hash.
173
        let mut header = Header {
174
            version,
175
            id: BlockId(Digest32::zero()),
8✔
176
            parent_id,
177
            ad_proofs_root,
178
            state_root,
179
            transaction_root,
180
            timestamp,
181
            n_bits,
182
            height,
183
            extension_root,
184
            autolykos_solution: autolykos_solution.clone(),
4✔
185
            votes,
186
        };
187

188
        let mut id_bytes = header.serialize_without_pow()?;
8✔
189
        let mut data = Vec::new();
4✔
190
        autolykos_solution.serialize_bytes(version, &mut data)?;
12✔
191
        id_bytes.extend(data);
4✔
192
        let id = BlockId(blake2b256_hash(&id_bytes).into());
4✔
193
        header.id = id;
4✔
194
        Ok(header)
4✔
195
    }
196
}
197

198
/// Solution for an Autolykos PoW puzzle. In Autolykos v.1 all the four fields are used, in
199
/// Autolykos v.2 only `miner_pk` and `nonce` fields are used.
200
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
201
#[derive(PartialEq, Eq, Debug, Clone)]
202
pub struct AutolykosSolution {
203
    /// Public key of miner. Part of Autolykos solution.
204
    #[cfg_attr(feature = "json", serde(rename = "pk"))]
205
    pub miner_pk: Box<EcPoint>,
206
    /// One-time public key. Prevents revealing of miners secret.
207
    #[cfg_attr(feature = "json", serde(default, rename = "w"))]
208
    pub pow_onetime_pk: Option<Box<EcPoint>>,
209
    /// nonce
210
    #[cfg_attr(
211
        feature = "json",
212
        serde(
213
            rename = "n",
214
            serialize_with = "crate::json::autolykos_solution::as_base16_string",
215
            deserialize_with = "crate::json::autolykos_solution::from_base16_string"
216
        )
217
    )]
218
    pub nonce: Vec<u8>,
219
    /// Distance between pseudo-random number, corresponding to nonce `nonce` and a secret,
220
    /// corresponding to `miner_pk`. The lower `pow_distance` is, the harder it was to find this
221
    /// solution.
222
    ///
223
    /// Note: we serialize/deserialize through custom functions since `BigInt`s serde implementation
224
    /// encodes the sign and absolute-value of the value separately, which is incompatible with the
225
    /// JSON representation used by Ergo. ASSUMPTION: we assume that `pow_distance` encoded as a
226
    /// `u64`.
227
    #[cfg_attr(
228
        feature = "json",
229
        serde(
230
            default,
231
            rename = "d",
232
            serialize_with = "crate::json::autolykos_solution::bigint_as_str",
233
            deserialize_with = "crate::json::autolykos_solution::bigint_from_serde_json_number"
234
        )
235
    )]
236
    pub pow_distance: Option<BigInt>,
237
}
238

239
impl AutolykosSolution {
240
    /// Serialize instance
241
    pub fn serialize_bytes<W: WriteSigmaVlqExt>(
22✔
242
        &self,
243
        version: u8,
244
        w: &mut W,
245
    ) -> Result<(), ScorexSerializationError> {
246
        if version == 1 {
24✔
247
            self.miner_pk.scorex_serialize(w)?;
18✔
248
            self.pow_onetime_pk
61✔
249
                .as_ref()
250
                .ok_or(ScorexSerializationError::Misc(
18✔
251
                    "pow_onetime_pk must == Some(_) for autolykos v1",
×
252
                ))?
253
                .scorex_serialize(w)?;
×
254
            w.write_all(&self.nonce)?;
21✔
255

256
            let d_bytes = self
63✔
257
                .pow_distance
×
258
                .as_ref()
259
                .ok_or(ScorexSerializationError::Misc(
23✔
260
                    "pow_distance must be == Some(_) for autolykos v1",
×
261
                ))?
262
                .to_signed_bytes_be();
263
            w.put_u8(d_bytes.len() as u8)?;
44✔
264
            w.write_all(&d_bytes)?;
49✔
265
        } else {
266
            // Autolykos v2
267
            self.miner_pk.scorex_serialize(w)?;
14✔
268
            w.write_all(&self.nonce)?;
34✔
269
        }
270
        Ok(())
21✔
271
    }
272
}
273

274
#[cfg(feature = "arbitrary")]
275
#[allow(clippy::unwrap_used)]
276
mod arbitrary {
277

278
    use crate::*;
279
    use num_bigint::BigInt;
280
    use proptest::array::{uniform3, uniform32};
281
    use proptest::prelude::*;
282

283
    use super::{AutolykosSolution, BlockId, Header, Votes};
284

285
    impl Arbitrary for Header {
286
        type Parameters = ();
287
        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
13✔
288
            (
289
                uniform32(1u8..),
14✔
290
                uniform32(1u8..),
13✔
291
                uniform32(1u8..),
16✔
292
                uniform32(1u8..),
14✔
293
                // Timestamps between 2000-2050
294
                946_674_000_000..2_500_400_300_000u64,
295
                any::<u32>(), // Note: n_bits must fit in u32
16✔
296
                1_000_000u32..10_000_000u32,
297
                prop::sample::select(vec![1_u8, 2]),
14✔
298
                any::<Box<AutolykosSolution>>(),
32✔
299
                uniform3(1u8..),
14✔
300
            )
301
                .prop_map(
302
                    |(
10✔
303
                        parent_id,
12✔
304
                        ad_proofs_root,
10✔
305
                        transaction_root,
12✔
306
                        extension_root,
10✔
307
                        timestamp,
12✔
308
                        n_bits,
10✔
309
                        height,
11✔
310
                        version,
11✔
311
                        autolykos_solution,
11✔
312
                        votes,
11✔
313
                    )| {
314
                        let parent_id = BlockId(Digest(parent_id));
11✔
315
                        let ad_proofs_root = Digest(ad_proofs_root);
11✔
316
                        let transaction_root = Digest(transaction_root);
11✔
317
                        let extension_root = Digest(extension_root);
11✔
318
                        let votes = Votes(votes);
11✔
319

320
                        // The `Header.id` field isn't serialized/deserialized but rather computed
321
                        // as a hash of every other field in `Header`. First we initialize header
322
                        // with dummy id field then compute the hash.
323
                        let mut header = Self {
12✔
324
                            version,
325
                            id: BlockId(Digest32::zero()),
22✔
326
                            parent_id,
327
                            ad_proofs_root,
328
                            state_root: ADDigest::zero(),
11✔
329
                            transaction_root,
330
                            timestamp,
331
                            n_bits: n_bits as u64,
12✔
332
                            height,
333
                            extension_root,
334
                            autolykos_solution: *autolykos_solution.clone(),
11✔
335
                            votes,
336
                        };
337
                        let mut id_bytes = header.serialize_without_pow().unwrap();
11✔
338
                        let mut data = Vec::new();
9✔
339
                        let mut w = &mut data;
9✔
340
                        autolykos_solution.serialize_bytes(version, &mut w).unwrap();
11✔
341
                        id_bytes.extend(data);
12✔
342
                        let id = BlockId(blake2b256_hash(&id_bytes));
12✔
343
                        header.id = id;
12✔
344

345
                        // Manually set the following parameters to `None` for autolykos v2. This is
346
                        // allowable since serialization/deserialization of the `Header` ignores
347
                        // these fields for version > 1.
348
                        if header.version > 1 {
24✔
349
                            header.autolykos_solution.pow_onetime_pk = None;
12✔
350
                            header.autolykos_solution.pow_distance = None;
12✔
351
                        }
352

353
                        header
12✔
354
                    },
355
                )
356
                .boxed()
357
        }
358

359
        type Strategy = BoxedStrategy<Header>;
360
    }
361

362
    impl Arbitrary for AutolykosSolution {
363
        type Parameters = ();
364
        type Strategy = BoxedStrategy<AutolykosSolution>;
365

366
        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
14✔
367
            (
368
                any::<Box<EcPoint>>(),
16✔
369
                prop::collection::vec(0_u8.., 8),
14✔
370
                any::<Box<EcPoint>>(),
16✔
371
                any::<u64>(),
14✔
372
            )
373
                .prop_map(
374
                    |(miner_pk, nonce, pow_onetime_pk, pow_distance)| AutolykosSolution {
34✔
375
                        miner_pk,
10✔
376
                        nonce,
12✔
377
                        pow_onetime_pk: Some(pow_onetime_pk),
10✔
378
                        pow_distance: Some(BigInt::from(pow_distance)),
22✔
379
                    },
380
                )
381
                .boxed()
382
        }
383
    }
384
}
385

386
#[allow(clippy::unwrap_used, clippy::panic)]
387
#[cfg(test)]
388
#[cfg(feature = "arbitrary")]
389
mod tests {
390
    use std::str::FromStr;
391

392
    use num_bigint::BigInt;
393

394
    use crate::header::Header;
395
    use proptest::prelude::*;
396
    use sigma_ser::scorex_serialize_roundtrip;
397

398
    proptest! {
399
        #![proptest_config(ProptestConfig::with_cases(64))]
400

401
        #[test]
402
        fn ser_roundtrip(v in any::<Header>()) {
403
            assert_eq![scorex_serialize_roundtrip(&v), v]
404
        }
405
    }
406

407
    #[test]
408
    fn parse_block_header() {
409
        let json = r#"{
410
            "extensionId": "d16f25b14457186df4c5f6355579cc769261ce1aebc8209949ca6feadbac5a3f",
411
            "difficulty": "626412390187008",
412
            "votes": "040000",
413
            "timestamp": 1618929697400,
414
            "size": 221,
415
            "stateRoot": "8ad868627ea4f7de6e2a2fe3f98fafe57f914e0f2ef3331c006def36c697f92713",
416
            "height": 471746,
417
            "nBits": 117586360,
418
            "version": 2,
419
            "id": "4caa17e62fe66ba7bd69597afdc996ae35b1ff12e0ba90c22ff288a4de10e91b",
420
            "adProofsRoot": "d882aaf42e0a95eb95fcce5c3705adf758e591532f733efe790ac3c404730c39",
421
            "transactionsRoot": "63eaa9aff76a1de3d71c81e4b2d92e8d97ae572a8e9ab9e66599ed0912dd2f8b",
422
            "extensionHash": "3f91f3c680beb26615fdec251aee3f81aaf5a02740806c167c0f3c929471df44",
423
            "powSolutions": {
424
              "pk": "02b3a06d6eaa8671431ba1db4dd427a77f75a5c2acbd71bfb725d38adc2b55f669",
425
              "w": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
426
              "n": "5939ecfee6b0d7f4",
427
              "d": "1234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
428
            },
429
            "adProofsId": "86eaa41f328bee598e33e52c9e515952ad3b7874102f762847f17318a776a7ae",
430
            "transactionsId": "ac80245714f25aa2fafe5494ad02a26d46e7955b8f5709f3659f1b9440797b3e",
431
            "parentId": "6481752bace5fa5acba5d5ef7124d48826664742d46c974c98a2d60ace229a34"
432
        }"#;
433
        let header: Header = serde_json::from_str(json).unwrap();
434
        assert_eq!(header.height, 471746);
435
        assert!(header.check_pow().unwrap());
436
        assert_eq!(
437
            header.autolykos_solution.pow_distance,
438
            Some(BigInt::from_str(
439
                "1234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
440
            )
441
            .unwrap())
442
        );
443
    }
444

445
    #[test]
446
    fn parse_block_header_explorer_v1() {
447
        // see https://api.ergoplatform.com/api/v1/blocks/de68a9cd727510d01eae3146f862261661f3bebdfd3c45c19d431b2ae81fb4b6
448
        let json = r#"{
449
            "extensionId": "d16f25b14457186df4c5f6355579cc769261ce1aebc8209949ca6feadbac5a3f",
450
            "difficulty": "626412390187008",
451
            "votes": [4,0,0],
452
            "timestamp": 1618929697400,
453
            "size": 221,
454
            "stateRoot": "8ad868627ea4f7de6e2a2fe3f98fafe57f914e0f2ef3331c006def36c697f92713",
455
            "height": 471746,
456
            "nBits": 117586360,
457
            "version": 2,
458
            "id": "4caa17e62fe66ba7bd69597afdc996ae35b1ff12e0ba90c22ff288a4de10e91b",
459
            "adProofsRoot": "d882aaf42e0a95eb95fcce5c3705adf758e591532f733efe790ac3c404730c39",
460
            "transactionsRoot": "63eaa9aff76a1de3d71c81e4b2d92e8d97ae572a8e9ab9e66599ed0912dd2f8b",
461
            "extensionHash": "3f91f3c680beb26615fdec251aee3f81aaf5a02740806c167c0f3c929471df44",
462
            "powSolutions": {
463
              "pk": "02b3a06d6eaa8671431ba1db4dd427a77f75a5c2acbd71bfb725d38adc2b55f669",
464
              "w": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
465
              "n": "5939ecfee6b0d7f4",
466
              "d": 1234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
467
            },
468
            "adProofsId": "86eaa41f328bee598e33e52c9e515952ad3b7874102f762847f17318a776a7ae",
469
            "transactionsId": "ac80245714f25aa2fafe5494ad02a26d46e7955b8f5709f3659f1b9440797b3e",
470
            "parentId": "6481752bace5fa5acba5d5ef7124d48826664742d46c974c98a2d60ace229a34"
471
        }"#;
472
        let header: Header = serde_json::from_str(json).unwrap();
473
        assert_eq!(header.height, 471746);
474
        assert!(header.check_pow().unwrap());
475
        assert_eq!(
476
            header.autolykos_solution.pow_distance,
477
            Some(BigInt::from_str(
478
                "1234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
479
            )
480
            .unwrap())
481
        );
482
    }
483
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc