Skip to content

Add autolykos 2 validation for custom message #804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 52 additions & 26 deletions ergo-chain-types/src/autolykos_pow_scheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Self, AutolykosPowSchemeError> {
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<BigUint, AutolykosPowSchemeError> {
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::<BigInt>();

// 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<BigUint, AutolykosPowSchemeError> {
if header.version == 1 {
Expand All @@ -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();
Expand All @@ -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)
}
}

Expand All @@ -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],
Expand Down Expand Up @@ -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<u32> {
pub fn gen_indexes(&self, seed_hash: &[u8; 32], big_n: u32) -> Vec<u32> {
let mut res = vec![];
let mut extended_hash: Vec<u8> = seed_hash.to_vec();
extended_hash.extend(&seed_hash[..3]);
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down Expand Up @@ -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 <https://github.com/ergoplatform/ergo/blob/f7b91c0be00531c6d042c10a8855149ca6924373/src/test/scala/org/ergoplatform/mining/AutolykosPowSchemeSpec.scala#L43-L130>
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions ergotree-interpreter/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ fn smethod_eval_fn(method: &SMethod) -> Result<EvalFn, EvalError> {
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",
Expand Down
74 changes: 72 additions & 2 deletions ergotree-interpreter/src/eval/sglobal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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;

Expand Down Expand Up @@ -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::<i32>()?
.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::<Vec<u8>>()?;
let nonce = args
.pop()
.ok_or_else(|| EvalError::NotFound("powHit: missing nonce".into()))?
.try_extract_into::<Vec<u8>>()?;
let msg = args
.pop()
.ok_or_else(|| EvalError::NotFound("powHit: missing msg".into()))?
.try_extract_into::<Vec<u8>>()?;
let k = args
.pop()
.ok_or_else(|| EvalError::NotFound("powHit: missing msg".into()))?
.try_extract_into::<i32>()?;
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)]
Expand All @@ -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;

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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) {
Expand Down
34 changes: 31 additions & 3 deletions ergotree-ir/src/types/sglobal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,7 +42,7 @@ pub const NONE_METHOD_ID: MethodId = MethodId(10);
lazy_static! {
/// Global method descriptors
pub(crate) static ref METHOD_DESC: Vec<SMethodDesc> =
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! {
Expand Down Expand Up @@ -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! {
Expand Down Expand Up @@ -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};
Expand All @@ -224,6 +247,11 @@ mod test {
).unwrap();
roundtrip_new_feature(&mc, ErgoTreeVersion::V3);
}
#[test]
fn pow_hit_roundtrip(k in any::<i32>(), msg in any::<Vec<u8>>(), nonce in any::<Vec<u8>>(), h in any::<Vec<u8>>(), 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]
Expand Down
11 changes: 11 additions & 0 deletions ergotree-ir/src/unsignedbigint256.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -169,6 +171,15 @@ impl UnsignedBigInt {
}
}

impl TryFrom<BigUint> for UnsignedBigInt {
type Error = String;

fn try_from(value: BigUint) -> Result<Self, Self::Error> {
let bytes = value.to_bytes_be();
Self::from_be_slice(&bytes).ok_or_else(|| "BigInt256 value: {value} out of bounds".into())
}
}

impl From<u32> for UnsignedBigInt {
fn from(value: u32) -> Self {
Self(U256::from(value))
Expand Down
Loading