diff --git a/ergo-chain-types/src/autolykos_pow_scheme.rs b/ergo-chain-types/src/autolykos_pow_scheme.rs index 3df666559..e8c45f7f3 100644 --- a/ergo-chain-types/src/autolykos_pow_scheme.rs +++ b/ergo-chain-types/src/autolykos_pow_scheme.rs @@ -13,7 +13,7 @@ use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; -use bounded_integer::{BoundedI32, BoundedU64}; +use bounded_integer::{BoundedU32, BoundedU64}; use derive_more::From; use k256::{elliptic_curve::PrimeField, Scalar}; use num_bigint::{BigInt, BigUint, Sign}; @@ -127,13 +127,52 @@ pub fn order_bigint() -> BigInt { #[derive(Debug, Clone, PartialEq, Eq)] pub struct AutolykosPowScheme { /// Represents the number of elements in one solution. **Important assumption**: `k <= 32`. - k: BoundedU64<1, 32>, + k: BoundedU64<2, 32>, /// Let `N` denote the initial table size. Then `n` is the value satisfying `N = 2 ^ n`. /// **Important assumption**: `n < 31`. - n: BoundedI32<1, 30>, + big_n_base: BoundedU32<16, { i32::MAX as u32 }>, } impl AutolykosPowScheme { + /// Create a new `AutolykosPowScheme`. Returns None if k is not >= 2 && <= 32 or big_n is not >= 16 + pub fn new(k: u64, big_n: u32) -> Result { + let k = BoundedU64::new(k).ok_or(AutolykosPowSchemeError::OutOfBounds)?; + let big_n = BoundedU32::new(big_n).ok_or(AutolykosPowSchemeError::OutOfBounds)?; + Ok(Self { + k, + big_n_base: big_n, + }) + } + + /// Calculate proof-of-work hit for an arbitrary message + pub fn pow_hit_message_v2( + &self, + msg: &[u8], + nonce: &[u8], + h: &[u8], + big_n: u32, + ) -> Result { + let seed_hash = self.calc_seed_v2(big_n, msg, nonce, h)?; + let indexes = self.gen_indexes(&seed_hash, big_n); + + let f2 = indexes + .into_iter() + .map(|idx| { + // This is specific to autolykos v2. + let mut concat = vec![]; + concat.extend_from_slice(&idx.to_be_bytes()); + concat.extend_from_slice(h); + concat.extend(&self.calc_big_m()); + BigInt::from_bytes_be(Sign::Plus, &blake2b256_hash(&concat)[1..]) + }) + .sum::(); + + // sum as byte array is always about 32 bytes + #[allow(clippy::unwrap_used)] + let array = as_unsigned_byte_array(32, f2).unwrap(); + Ok(BigUint::from_bytes_be(&*blake2b256_hash(&array))) + } + /// Get hit for Autolykos header (to test it then against PoW target) pub fn pow_hit(&self, header: &Header) -> Result { if header.version == 1 { @@ -144,7 +183,6 @@ impl AutolykosPowScheme { .cloned() .ok_or(AutolykosPowSchemeError::MissingPowDistanceParameter) } else { - // hit for version 2 let msg = blake2b256_hash(&header.serialize_without_pow()?).to_vec(); let nonce = header.autolykos_solution.nonce.clone(); let height_bytes = header.height.to_be_bytes(); @@ -154,22 +192,7 @@ impl AutolykosPowScheme { // `N` from autolykos paper let big_n = self.calc_big_n(header.version, header.height); - let seed_hash = self.calc_seed_v2(big_n, &msg, &nonce, &height_bytes)?; - let indexes = self.gen_indexes(&seed_hash, big_n); - - let f2 = indexes.into_iter().fold(BigInt::from(0u32), |acc, idx| { - // This is specific to autolykos v2. - let mut concat = vec![]; - concat.extend_from_slice(&idx.to_be_bytes()); - concat.extend(&height_bytes); - concat.extend(&self.calc_big_m()); - acc + BigInt::from_bytes_be(Sign::Plus, &blake2b256_hash(&concat)[1..]) - }); - - // sum as byte array is always about 32 bytes - #[allow(clippy::unwrap_used)] - let array = as_unsigned_byte_array(32, f2).unwrap(); - Ok(BigUint::from_bytes_be(&*blake2b256_hash(&array))) + self.pow_hit_message_v2(&msg, &nonce, &height_bytes, big_n) } } @@ -182,7 +205,7 @@ impl AutolykosPowScheme { /// in ErgoPow paper. pub fn calc_seed_v2( &self, - big_n: usize, + big_n: u32, msg: &[u8], nonce: &[u8], header_height_bytes: &[u8], @@ -212,7 +235,7 @@ impl AutolykosPowScheme { } /// Returns a list of size `k` with numbers in [0,`N`) - pub fn gen_indexes(&self, seed_hash: &[u8; 32], big_n: usize) -> Vec { + pub fn gen_indexes(&self, seed_hash: &[u8; 32], big_n: u32) -> Vec { let mut res = vec![]; let mut extended_hash: Vec = seed_hash.to_vec(); extended_hash.extend(&seed_hash[..3]); @@ -229,9 +252,9 @@ impl AutolykosPowScheme { } /// Calculates table size (N value) for a given height (moment of time) - pub fn calc_big_n(&self, header_version: u8, header_height: u32) -> usize { + pub fn calc_big_n(&self, header_version: u8, header_height: u32) -> u32 { // Number of elements in a table to find k-sum problem solution on top of - let n_base = 2i32.pow(self.n.get() as u32) as usize; + let n_base = self.big_n_base.get(); if header_version == 1 { n_base } else { @@ -259,7 +282,7 @@ impl Default for AutolykosPowScheme { #[allow(clippy::unwrap_used)] AutolykosPowScheme { k: BoundedU64::new(32).unwrap(), - n: BoundedI32::new(26).unwrap(), + big_n_base: BoundedU32::new(2u32.pow(26)).unwrap(), } } } @@ -299,6 +322,9 @@ pub enum AutolykosPowSchemeError { /// Checking proof-of-work for AutolykosV1 is not supported #[error("Header.check_pow is not supported for Autolykos1")] Unsupported, + /// k or N are out of bounds, see [`AutolykosPowScheme::new`] + #[error("Arguments to AutolykosPowScheme::new were out of bounds")] + OutOfBounds, } /// The following tests are taken from @@ -313,7 +339,7 @@ mod tests { #[test] fn test_calc_big_n() { let pow = AutolykosPowScheme::default(); - let n_base = 2i32.pow(pow.n.get() as u32) as usize; + let n_base = pow.big_n_base.get(); // autolykos v1 assert_eq!(pow.calc_big_n(1, 700000), n_base); diff --git a/ergotree-interpreter/src/eval.rs b/ergotree-interpreter/src/eval.rs index f81b860df..356a48261 100644 --- a/ergotree-interpreter/src/eval.rs +++ b/ergotree-interpreter/src/eval.rs @@ -365,6 +365,7 @@ fn smethod_eval_fn(method: &SMethod) -> Result { sglobal::NONE_METHOD_ID => self::sglobal::SGLOBAL_NONE_EVAL_FN, sglobal::ENCODE_NBITS_METHOD_ID => self::sglobal::ENCODE_NBITS_EVAL_FN, sglobal::DECODE_NBITS_METHOD_ID => self::sglobal::DECODE_NBITS_EVAL_FN, + sglobal::POW_HIT_METHOD_ID => self::sglobal::POW_HIT_EVAL_FN, method_id => { return Err(EvalError::NotFound(format!( "Eval fn: method {:?} with method id {:?} not found in SGlobal", diff --git a/ergotree-interpreter/src/eval/sglobal.rs b/ergotree-interpreter/src/eval/sglobal.rs index f631a7ad6..9d3afb81d 100644 --- a/ergotree-interpreter/src/eval/sglobal.rs +++ b/ergotree-interpreter/src/eval/sglobal.rs @@ -4,6 +4,7 @@ use alloc::{string::ToString, sync::Arc}; use ergo_chain_types::autolykos_pow_scheme::{decode_compact_bits, encode_compact_bits}; use ergotree_ir::serialization::sigma_byte_writer::SigmaByteWrite; +use ergotree_ir::unsignedbigint256::UnsignedBigInt; use ergotree_ir::{ mir::{ constant::{Constant, TryExtractInto}, @@ -19,7 +20,7 @@ use num_bigint::BigInt; use super::EvalFn; use crate::eval::Vec; -use ergo_chain_types::ec_point::generator; +use ergo_chain_types::{autolykos_pow_scheme::AutolykosPowScheme, ec_point::generator}; use ergotree_ir::bigint256::BigInt256; use ergotree_ir::types::stype::SType; @@ -245,6 +246,41 @@ pub(crate) static DECODE_NBITS_EVAL_FN: EvalFn = |_mc, _env, _ctx, _obj, args| { .map_err(EvalError::UnexpectedValue)?, )) }; +pub(crate) static POW_HIT_EVAL_FN: EvalFn = |_mc, _env, _ctx, _obj, mut args| { + // Pop arguments to avoid cloning + let big_n: u32 = args + .pop() + .ok_or_else(|| EvalError::NotFound("powHit: missing N".into()))? + .try_extract_into::()? + .try_into() + .map_err(|_| EvalError::Misc("N out of bounds".into()))?; + let h = args + .pop() + .ok_or_else(|| EvalError::NotFound("powHit: missing h".into()))? + .try_extract_into::>()?; + let nonce = args + .pop() + .ok_or_else(|| EvalError::NotFound("powHit: missing nonce".into()))? + .try_extract_into::>()?; + let msg = args + .pop() + .ok_or_else(|| EvalError::NotFound("powHit: missing msg".into()))? + .try_extract_into::>()?; + let k = args + .pop() + .ok_or_else(|| EvalError::NotFound("powHit: missing msg".into()))? + .try_extract_into::()?; + Ok(UnsignedBigInt::try_from( + AutolykosPowScheme::new( + k.try_into() + .map_err(|_| EvalError::Misc("k out of bounds".into()))?, + big_n, + )? + .pow_hit_message_v2(&msg, &nonce, &h, big_n)?, + ) + .map_err(EvalError::Misc)? + .into()) +}; #[allow(clippy::unwrap_used)] #[cfg(test)] @@ -271,8 +307,10 @@ mod tests { use crate::eval::test_util::{eval_out, eval_out_wo_ctx, try_eval_out_with_version}; use ergotree_ir::chain::context::Context; use ergotree_ir::types::sglobal::{ - self, DECODE_NBITS_METHOD, DESERIALIZE_METHOD, ENCODE_NBITS_METHOD, SERIALIZE_METHOD, + self, DECODE_NBITS_METHOD, DESERIALIZE_METHOD, ENCODE_NBITS_METHOD, POW_HIT_METHOD, + SERIALIZE_METHOD, }; + use ergotree_ir::types::stype::SType; use sigma_test_util::force_any_val; @@ -367,6 +405,23 @@ mod tests { } } + fn pow_hit(k: u32, msg: &[u8], nonce: &[u8], h: &[u8], big_n: u32) -> UnsignedBigInt { + let expr: Expr = MethodCall::new( + Expr::Global, + POW_HIT_METHOD.clone(), + vec![ + Constant::from(k as i32).into(), + Constant::from(msg.to_owned()).into(), + Constant::from(nonce.to_owned()).into(), + Constant::from(h.to_owned()).into(), + Constant::from(big_n as i32).into(), + ], + ) + .unwrap() + .into(); + eval_out_wo_ctx(&expr) + } + #[test] fn eval_group_generator() { let expr: Expr = PropertyCall::new(Expr::Global, sglobal::GROUP_GENERATOR_METHOD.clone()) @@ -659,6 +714,21 @@ mod tests { ); } + #[test] + fn pow_hit_eval() { + let msg = base16::decode("0a101b8c6a4f2e").unwrap(); + let nonce = base16::decode("000000000000002c").unwrap(); + let hbs = base16::decode("00000000").unwrap(); + assert_eq!( + pow_hit(32, &msg, &nonce, &hbs, 1024 * 1024), + UnsignedBigInt::from_str_radix( + "326674862673836209462483453386286740270338859283019276168539876024851191344", + 10 + ) + .unwrap() + ); + } + proptest! { #[test] fn serialize_sigmaprop_eq_prop_bytes(sigma_prop: SigmaProp) { diff --git a/ergotree-ir/src/types/sglobal.rs b/ergotree-ir/src/types/sglobal.rs index 4b4555ea1..59ee6f73d 100644 --- a/ergotree-ir/src/types/sglobal.rs +++ b/ergotree-ir/src/types/sglobal.rs @@ -32,6 +32,8 @@ pub const FROM_BIGENDIAN_BYTES_METHOD_ID: MethodId = MethodId(5); pub const ENCODE_NBITS_METHOD_ID: MethodId = MethodId(6); /// decodeNBits method id (v6.0) pub const DECODE_NBITS_METHOD_ID: MethodId = MethodId(7); +/// Global.powHit function +pub const POW_HIT_METHOD_ID: MethodId = MethodId(8); /// "some" property pub const SOME_METHOD_ID: MethodId = MethodId(9); /// "none" property @@ -40,7 +42,7 @@ pub const NONE_METHOD_ID: MethodId = MethodId(10); lazy_static! { /// Global method descriptors pub(crate) static ref METHOD_DESC: Vec = - vec![GROUP_GENERATOR_METHOD_DESC.clone(), XOR_METHOD_DESC.clone(), SERIALIZE_METHOD_DESC.clone(), DESERIALIZE_METHOD_DESC.clone(), FROM_BIGENDIAN_BYTES_METHOD_DESC.clone(), ENCODE_NBITS_METHOD_DESC.clone(), DECODE_NBITS_METHOD_DESC.clone(), NONE_METHOD_DESC.clone(), SOME_METHOD_DESC.clone()]; + vec![GROUP_GENERATOR_METHOD_DESC.clone(), XOR_METHOD_DESC.clone(), SERIALIZE_METHOD_DESC.clone(), DESERIALIZE_METHOD_DESC.clone(), FROM_BIGENDIAN_BYTES_METHOD_DESC.clone(), ENCODE_NBITS_METHOD_DESC.clone(), DECODE_NBITS_METHOD_DESC.clone(), NONE_METHOD_DESC.clone(), SOME_METHOD_DESC.clone(), POW_HIT_METHOD_DESC.clone()]; } lazy_static! { @@ -163,6 +165,27 @@ lazy_static! { }; /// GLOBAL.serialize pub static ref SERIALIZE_METHOD: SMethod = SMethod::new(STypeCompanion::Global, SERIALIZE_METHOD_DESC.clone(),); + + static ref POW_HIT_METHOD_DESC: SMethodDesc = SMethodDesc { + method_id: POW_HIT_METHOD_ID, + name: "powHit", + tpe: SFunc { + t_dom: vec![ + SType::SGlobal, + SType::SInt, + SType::SColl(SType::SByte.into()), + SType::SColl(SType::SByte.into()), + SType::SColl(SType::SByte.into()), + SType::SInt, + ], + t_range: SType::SBoolean.into(), + tpe_params: vec![], + }, + explicit_type_args: vec![], + min_version: ErgoTreeVersion::V3 + }; + /// Global.powHit + pub static ref POW_HIT_METHOD: SMethod = SMethod::new(STypeCompanion::Global, POW_HIT_METHOD_DESC.clone()); } lazy_static! { @@ -206,9 +229,9 @@ mod test { use crate::{ bigint256::BigInt256, ergo_tree::ErgoTreeVersion, - mir::{expr::Expr, method_call::MethodCall}, + mir::{constant::Constant, expr::Expr, method_call::MethodCall}, serialization::roundtrip_new_feature, - types::{stype::SType, stype_param::STypeVar}, + types::{sglobal::POW_HIT_METHOD, stype::SType, stype_param::STypeVar}, }; use super::{DECODE_NBITS_METHOD, DESERIALIZE_METHOD, ENCODE_NBITS_METHOD}; @@ -224,6 +247,11 @@ mod test { ).unwrap(); roundtrip_new_feature(&mc, ErgoTreeVersion::V3); } + #[test] + fn pow_hit_roundtrip(k in any::(), msg in any::>(), nonce in any::>(), h in any::>(), big_n: u32) { + let mc = MethodCall::new(Expr::Global, POW_HIT_METHOD.clone(), vec![Constant::from(k).into(), Constant::from(msg).into(), Constant::from(nonce).into(), Constant::from(h).into(), Constant::from(big_n as i32).into()]).unwrap(); + roundtrip_new_feature(&mc, ErgoTreeVersion::V3); + } } #[test] diff --git a/ergotree-ir/src/unsignedbigint256.rs b/ergotree-ir/src/unsignedbigint256.rs index f94757f0a..9abfc723e 100644 --- a/ergotree-ir/src/unsignedbigint256.rs +++ b/ergotree-ir/src/unsignedbigint256.rs @@ -1,6 +1,8 @@ //! 256-bit unsigned big integer type +use alloc::string::String; use alloc::vec::Vec; use core::ops::{Div, Mul, Rem}; +use num_bigint::BigUint; use bnum::{ cast::{As, CastFrom}, @@ -169,6 +171,15 @@ impl UnsignedBigInt { } } +impl TryFrom for UnsignedBigInt { + type Error = String; + + fn try_from(value: BigUint) -> Result { + let bytes = value.to_bytes_be(); + Self::from_be_slice(&bytes).ok_or_else(|| "BigInt256 value: {value} out of bounds".into()) + } +} + impl From for UnsignedBigInt { fn from(value: u32) -> Self { Self(U256::from(value))