Skip to content

Commit 2dcbfdc

Browse files
authored
Merge pull request #824 from sethdusek/nbits6.0
[6.0] Add encodeNBits/decodeNBits
2 parents 2185bf7 + 3e3ed84 commit 2dcbfdc

File tree

9 files changed

+269
-20
lines changed

9 files changed

+269
-20
lines changed

bindings/ergo-lib-python/src/chain/header.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl Header {
8181
self.0.timestamp
8282
}
8383
#[getter]
84-
fn n_bits(&self) -> u64 {
84+
fn n_bits(&self) -> u32 {
8585
self.0.n_bits
8686
}
8787
#[getter]

ergo-chain-generation/src/chain_generation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ fn prove_block(
135135
extension_candidate: ExtensionCandidate,
136136
) -> Option<ErgoFullBlock> {
137137
// Corresponds to initial difficulty of 1, in line with the ergo test suite.
138-
let n_bits = 16842752_u64;
138+
let n_bits = 16842752_u32;
139139
let state_root = ADDigest::zero();
140140
let votes = Votes([0, 0, 0]);
141141

ergo-chain-generation/src/fake_pow_scheme.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ mod tests {
7979
extension_candidate: ExtensionCandidate,
8080
) -> Option<ErgoFullBlock> {
8181
// Corresponds to initial difficulty of 1, in line with the ergo test suite.
82-
let n_bits = 16842752_u64;
82+
let n_bits = 16842752_u32;
8383
let state_root = ADDigest::zero();
8484
let votes = Votes([0, 0, 0]);
8585

ergo-chain-types/src/autolykos_pow_scheme.rs

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,35 +54,69 @@ use crate::Header;
5454
/// Bitcoin only uses this "compact" format for encoding difficulty targets, which are unsigned
5555
/// 256bit quantities. Thus, all the complexities of the sign bit and using base 256 are probably
5656
/// an implementation accident.
57-
pub fn decode_compact_bits(n_bits: u64) -> BigInt {
57+
pub fn decode_compact_bits(n_bits: u32) -> BigInt {
5858
let compact = n_bits as i64;
5959
let size = ((compact >> 24) as i32) & 0xFF;
6060
if size == 0 {
6161
return BigInt::from(0);
6262
}
63-
let mut buf: Vec<i8> = vec![0; size as usize];
63+
let mut buf: Vec<u8> = vec![0; size as usize];
6464
if size >= 1 {
6565
// Store the first byte of the mantissa
66-
buf[0] = (((compact >> 16) as i32) & 0xFF) as i8;
66+
buf[0] = (compact >> 16 & 0xFF) as u8;
6767
}
6868
if size >= 2 {
69-
buf[1] = (((compact >> 8) as i32) & 0xFF) as i8;
69+
buf[1] = (compact >> 8 & 0xFF) as u8;
7070
}
7171
if size >= 3 {
72-
buf[2] = ((compact as i32) & 0xFF) as i8;
72+
buf[2] = (compact & 0xFF) as u8;
7373
}
7474

75-
let is_negative = (buf[0] as i32) & 0x80 == 0x80;
75+
let is_negative = buf[0] & 0x80 == 0x80;
7676
if is_negative {
7777
buf[0] &= 0x7f;
78-
let buf: Vec<_> = buf.into_iter().map(|x| x as u8).collect();
7978
-BigInt::from_signed_bytes_be(&buf)
8079
} else {
81-
let buf: Vec<_> = buf.into_iter().map(|x| x as u8).collect();
8280
BigInt::from_signed_bytes_be(&buf)
8381
}
8482
}
8583

84+
/// Encode BigInt in 32-bit compact format. See: `decode_compact_bits` for more information
85+
pub fn encode_compact_bits(bigint: &BigInt) -> i64 {
86+
// truncate input to a 64-bit signed value, equivalent to Scala's BigInt.longValue method
87+
// this is used to replicate reference implementation's quirks when handling negative numbers
88+
fn truncate(input: &BigInt) -> i64 {
89+
let mut bytes = input.to_signed_bytes_le();
90+
for _ in 0..size_of::<i64>().saturating_sub(bytes.len()) {
91+
if input.sign() == Sign::Minus {
92+
bytes.push(0xff); // sign extension
93+
} else {
94+
bytes.push(0x0);
95+
}
96+
}
97+
#[allow(clippy::unwrap_used)] // size of bytes is guaranteed to be == 8 (size_of::<i64>())
98+
i64::from_le_bytes(bytes[0..size_of::<i64>()].try_into().unwrap())
99+
}
100+
let bytes = bigint.to_signed_bytes_be();
101+
let mut size = bytes.len() as i64;
102+
let mut result = if size < 3 {
103+
truncate(bigint) << (8 * (3 - size))
104+
} else {
105+
truncate(&(bigint >> (8 * (size - 3))))
106+
};
107+
// top-most bit of mantissa is used to indicate sign, if it's set then shift result and increase exponent
108+
if result & 0x00800000 != 0 {
109+
result >>= 8;
110+
size += 1;
111+
}
112+
result |= size << 24;
113+
if bigint.sign() == Sign::Minus {
114+
result | 0x00800000
115+
} else {
116+
result
117+
}
118+
}
119+
86120
/// Order of the secp256k1 elliptic curve as BigInt
87121
pub fn order_bigint() -> BigInt {
88122
#[allow(clippy::unwrap_used)]
@@ -439,4 +473,59 @@ mod tests {
439473
let n_bits = 16842752;
440474
assert_eq!(decode_compact_bits(n_bits), BigInt::from(1_u8));
441475
}
476+
#[test]
477+
fn test_encode_compact_bits() {
478+
assert_eq!(
479+
encode_compact_bits(
480+
&BigInt::from_str_radix("1bc330000000000000000000000000000000000000000000", 16)
481+
.unwrap()
482+
),
483+
0x181bc330
484+
);
485+
assert_eq!(
486+
encode_compact_bits(&BigInt::from_str_radix("12345600", 16).unwrap()),
487+
0x04123456
488+
);
489+
// The bitcoin test suite has an output value of 0x04923456 for this.
490+
// But to match the reference Scala impl we handle negative numbers differently and produce a negative nBits value
491+
assert_eq!(
492+
encode_compact_bits(&BigInt::from_str_radix("-12345600", 16).unwrap()),
493+
-0x1235
494+
);
495+
}
496+
#[cfg(feature = "arbitrary")]
497+
mod proptests {
498+
use num_bigint::{BigInt, Sign};
499+
use num_traits::Zero;
500+
use proptest::prelude::*;
501+
502+
use crate::autolykos_pow_scheme::{decode_compact_bits, encode_compact_bits};
503+
// check if two bigints have their most significant 3 bytes equal, and have the same exponent
504+
fn approx_equal(a: &BigInt, b: &BigInt) -> bool {
505+
if a == b {
506+
return true;
507+
} else if a.is_zero() || b.is_zero() {
508+
return false;
509+
}
510+
let (exp_a, mantissa_a) = a.iter_u32_digits().enumerate().last().unwrap();
511+
let (exp_b, mantissa_b) = a.iter_u32_digits().enumerate().last().unwrap();
512+
mantissa_a == mantissa_b && exp_a == exp_b && a.sign() == b.sign()
513+
}
514+
515+
fn bigint_strategy() -> impl Strategy<Value = BigInt> {
516+
(any::<bool>(), proptest::collection::vec(any::<u8>(), 0..64)).prop_map(
517+
|(negative, bytes)| {
518+
BigInt::from_bytes_be(if negative { Sign::Minus } else { Sign::Plus }, &bytes)
519+
},
520+
)
521+
}
522+
523+
proptest! {
524+
#[test]
525+
fn nbits_roundtrip(a in bigint_strategy()) {
526+
let roundtripped = decode_compact_bits(encode_compact_bits(&a) as u32);
527+
assert!(approx_equal(&roundtripped, &a))
528+
}
529+
}
530+
}
442531
}

ergo-chain-types/src/header.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub struct Header {
4242
pub timestamp: u64,
4343
/// Current difficulty in a compressed view.
4444
#[cfg_attr(feature = "json", serde(rename = "nBits"))]
45-
pub n_bits: u64,
45+
pub n_bits: u32,
4646
/// Block height
4747
#[cfg_attr(feature = "json", serde(rename = "height"))]
4848
pub height: u32,
@@ -73,7 +73,7 @@ impl Header {
7373

7474
// n_bits needs to be serialized in big-endian format. Note that it actually fits in a
7575
// `u32`.
76-
let n_bits_be = (self.n_bits as u32).to_be_bytes();
76+
let n_bits_be = self.n_bits.to_be_bytes();
7777
w.write_all(&n_bits_be)?;
7878

7979
w.put_u32(self.height)?;
@@ -119,7 +119,7 @@ impl ScorexSerializable for Header {
119119
let extension_root = Digest32::scorex_parse(r)?;
120120
let mut n_bits_buf = [0u8, 0, 0, 0];
121121
r.read_exact(&mut n_bits_buf)?;
122-
let n_bits = u32::from_be_bytes(n_bits_buf) as u64;
122+
let n_bits = u32::from_be_bytes(n_bits_buf);
123123
let height = r.get_u32()?;
124124
let mut votes_bytes = [0u8, 0, 0];
125125
r.read_exact(&mut votes_bytes)?;
@@ -327,7 +327,7 @@ mod arbitrary {
327327
state_root: ADDigest::zero(),
328328
transaction_root,
329329
timestamp,
330-
n_bits: n_bits as u64,
330+
n_bits,
331331
height,
332332
extension_root,
333333
autolykos_solution: *autolykos_solution.clone(),

ergo-chain-types/src/preheader.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct PreHeader {
1414
/// Timestamp of a block in ms from UNIX epoch
1515
pub timestamp: u64,
1616
/// Current difficulty in a compressed view.
17-
pub n_bits: u64,
17+
pub n_bits: u32,
1818
/// Block height
1919
pub height: u32,
2020
/// Public key of miner
@@ -53,7 +53,7 @@ mod arbitrary {
5353
uniform32(1u8..),
5454
// Timestamps between 2000-2050
5555
946_674_000_000..2_500_400_300_000u64,
56-
any::<u64>(),
56+
any::<u32>(),
5757
1_000_000u32..10_000_000u32,
5858
any::<Box<EcPoint>>(),
5959
uniform3(1u8..),

ergotree-interpreter/src/eval.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ fn smethod_eval_fn(method: &SMethod) -> Result<EvalFn, EvalError> {
362362
sglobal::SERIALIZE_METHOD_ID => self::sglobal::SERIALIZE_EVAL_FN,
363363
sglobal::SOME_METHOD_ID => self::sglobal::SGLOBAL_SOME_EVAL_FN,
364364
sglobal::NONE_METHOD_ID => self::sglobal::SGLOBAL_NONE_EVAL_FN,
365+
sglobal::ENCODE_NBITS_METHOD_ID => self::sglobal::ENCODE_NBITS_EVAL_FN,
366+
sglobal::DECODE_NBITS_METHOD_ID => self::sglobal::DECODE_NBITS_EVAL_FN,
365367
method_id => {
366368
return Err(EvalError::NotFound(format!(
367369
"Eval fn: method {:?} with method id {:?} not found in SGlobal",

ergotree-interpreter/src/eval/sglobal.rs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::eval::EvalError;
22
use alloc::boxed::Box;
33
use alloc::{string::ToString, sync::Arc};
44

5+
use ergo_chain_types::autolykos_pow_scheme::{decode_compact_bits, encode_compact_bits};
56
use ergotree_ir::serialization::sigma_byte_writer::SigmaByteWrite;
67
use ergotree_ir::{
78
mir::{
@@ -14,6 +15,7 @@ use ergotree_ir::{
1415
sigma_byte_writer::SigmaByteWriter,
1516
},
1617
};
18+
use num_bigint::BigInt;
1719

1820
use super::EvalFn;
1921
use crate::eval::Vec;
@@ -219,6 +221,31 @@ pub(crate) static SGLOBAL_NONE_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| {
219221
Ok(Value::Opt(None))
220222
};
221223

224+
pub(crate) static ENCODE_NBITS_EVAL_FN: EvalFn = |_mc, _env, _ctx, _obj, args| {
225+
let bigint: BigInt = args
226+
.first()
227+
.cloned()
228+
.ok_or_else(|| EvalError::NotFound("encodeNBits: missing first argument".into()))?
229+
.try_extract_into::<BigInt256>()?
230+
.into();
231+
Ok(Value::Long(encode_compact_bits(&bigint)))
232+
};
233+
234+
pub(crate) static DECODE_NBITS_EVAL_FN: EvalFn = |_mc, _env, _ctx, _obj, args| {
235+
let nbits: i64 = args
236+
.first()
237+
.cloned()
238+
.ok_or_else(|| EvalError::NotFound("decodeNBits: missing first argument".into()))?
239+
.try_extract_into()?;
240+
// truncation is safe here, since only bottom 4 bytes are used in decode.
241+
// nbits is only i64 because Scala doesn't have an unsigned 32-bit type
242+
Ok(Value::BigInt(
243+
decode_compact_bits(nbits as u32)
244+
.try_into()
245+
.map_err(EvalError::UnexpectedValue)?,
246+
))
247+
};
248+
222249
#[allow(clippy::unwrap_used)]
223250
#[cfg(test)]
224251
#[cfg(feature = "arbitrary")]
@@ -238,11 +265,14 @@ mod tests {
238265
use ergotree_ir::types::sgroup_elem::GET_ENCODED_METHOD;
239266
use ergotree_ir::types::stype_param::STypeVar;
240267
use ergotree_ir::unsignedbigint256::UnsignedBigInt;
268+
use num_traits::Num;
241269
use proptest::proptest;
242270

243271
use crate::eval::tests::{eval_out, eval_out_wo_ctx, try_eval_out_with_version};
244272
use ergotree_ir::chain::context::Context;
245-
use ergotree_ir::types::sglobal::{self, DESERIALIZE_METHOD, SERIALIZE_METHOD};
273+
use ergotree_ir::types::sglobal::{
274+
self, DECODE_NBITS_METHOD, DESERIALIZE_METHOD, ENCODE_NBITS_METHOD, SERIALIZE_METHOD,
275+
};
246276
use ergotree_ir::types::stype::SType;
247277
use sigma_test_util::force_any_val;
248278

@@ -291,6 +321,28 @@ mod tests {
291321
.unwrap()
292322
}
293323

324+
fn encode_nbits(bigint: BigInt256) -> i64 {
325+
let mc: Expr = MethodCall::new(
326+
Expr::Global,
327+
ENCODE_NBITS_METHOD.clone(),
328+
vec![bigint.into()],
329+
)
330+
.unwrap()
331+
.into();
332+
eval_out_wo_ctx(&mc)
333+
}
334+
335+
fn decode_nbits(nbits: i64) -> BigInt256 {
336+
let mc: Expr = MethodCall::new(
337+
Expr::Global,
338+
DECODE_NBITS_METHOD.clone(),
339+
vec![nbits.into()],
340+
)
341+
.unwrap()
342+
.into();
343+
eval_out_wo_ctx(&mc)
344+
}
345+
294346
fn create_some_none_method_call<T>(value: Option<T>, tpe: SType) -> Expr
295347
where
296348
T: Into<Constant>,
@@ -347,6 +399,55 @@ mod tests {
347399
);
348400
}
349401

402+
#[test]
403+
fn test_eval_encode_nbits() {
404+
assert_eq!(
405+
encode_nbits(
406+
BigInt256::from_str_radix("1bc330000000000000000000000000000000000000000000", 16)
407+
.unwrap()
408+
),
409+
0x181bc330
410+
);
411+
412+
assert_eq!(
413+
encode_nbits(BigInt256::from_str_radix("12345600", 16).unwrap()),
414+
0x04123456
415+
);
416+
assert_eq!(
417+
encode_nbits(BigInt256::from_str_radix("-12345600", 16).unwrap()),
418+
-0x1235
419+
);
420+
}
421+
422+
#[test]
423+
fn test_eval_decode_nbits() {
424+
// Following example taken from https://btcinformation.org/en/developer-reference#target-nbits
425+
let n_bits = 0x181bc330;
426+
assert_eq!(
427+
decode_nbits(n_bits),
428+
BigInt256::from_str_radix("1bc330000000000000000000000000000000000000000000", 16)
429+
.unwrap()
430+
);
431+
432+
let n_bits = 0x01003456;
433+
assert_eq!(decode_nbits(n_bits), 0x00.into());
434+
435+
let n_bits = 0x01123456;
436+
assert_eq!(decode_nbits(n_bits), 0x12.into());
437+
438+
let n_bits = 0x04923456;
439+
assert_eq!(decode_nbits(n_bits), (-0x12345600i64).into());
440+
441+
let n_bits = 0x04123456;
442+
assert_eq!(decode_nbits(n_bits), 0x12345600.into());
443+
444+
let n_bits = 0x05123456;
445+
assert_eq!(decode_nbits(n_bits), 0x1234560000i64.into());
446+
447+
let n_bits = 16842752;
448+
assert_eq!(decode_nbits(n_bits), BigInt256::from(1_i8));
449+
}
450+
350451
use proptest::prelude::*;
351452

352453
proptest! {

0 commit comments

Comments
 (0)