diff --git a/ergo-chain-generation/src/chain_generation.rs b/ergo-chain-generation/src/chain_generation.rs index 20ded310b..812b9de36 100644 --- a/ergo-chain-generation/src/chain_generation.rs +++ b/ergo-chain-generation/src/chain_generation.rs @@ -3,6 +3,7 @@ use std::convert::TryFrom; +use ergo_chain_types::autolykos_pow_scheme::{decode_compact_bits, order_bigint}; use ergo_lib::ergotree_ir::chain::context_extension::ContextExtension; use ergo_lib::{ chain::{ @@ -10,7 +11,6 @@ use ergo_lib::{ transaction::{prover_result::ProverResult, Input, Transaction, TxIoVec}, }, ergo_chain_types::{BlockId, Digest32}, - ergotree_ir::sigma_protocol::dlog_group::order_bigint, }; use ergo_lib::{ ergo_chain_types::ADDigest, @@ -197,7 +197,7 @@ fn prove_block( .to_vec(); // Order of the secp256k1 elliptic curve let order = order_bigint(); - let target_b = order.clone() / ergo_nipopow::decode_compact_bits(header.n_bits); + let target_b = order.clone() / decode_compact_bits(header.n_bits); let x = DlogProverInput::random(); let x_bigint = BigInt::from_bytes_be(Sign::Plus, &x.to_bytes()); diff --git a/ergo-chain-generation/src/fake_pow_scheme.rs b/ergo-chain-generation/src/fake_pow_scheme.rs index bf464bf47..6c7d49a8e 100644 --- a/ergo-chain-generation/src/fake_pow_scheme.rs +++ b/ergo-chain-generation/src/fake_pow_scheme.rs @@ -8,10 +8,9 @@ #[cfg(test)] mod tests { use ergo_lib::ergo_chain_types::{blake2b256_hash, ADDigest, BlockId, Digest32}; - use ergo_lib::ergotree_ir::sigma_protocol::dlog_group::order_bigint; use ergo_nipopow::{NipopowAlgos, NipopowProof}; - use ergo_chain_types::{AutolykosSolution, Header, Votes}; + use ergo_chain_types::{autolykos_pow_scheme::order_bigint, AutolykosSolution, Header, Votes}; use ergo_lib::ergotree_interpreter::sigma_protocol::private_input::DlogProverInput; use ergo_lib::ergotree_ir::serialization::sigma_byte_writer::SigmaByteWriter; use ergo_nipopow::PoPowHeader; diff --git a/ergo-chain-types/Cargo.toml b/ergo-chain-types/Cargo.toml index e00b6f556..1c0e6b13d 100644 --- a/ergo-chain-types/Cargo.toml +++ b/ergo-chain-types/Cargo.toml @@ -10,6 +10,7 @@ exclude = ["proptest-regressions/*"] [dependencies] thiserror = { workspace = true } +bounded-integer = { workspace = true } derive_more = { workspace = true } sigma-ser = { workspace = true } sigma-util = { workspace = true } diff --git a/ergo-nipopow/src/autolykos_pow_scheme.rs b/ergo-chain-types/src/autolykos_pow_scheme.rs similarity index 69% rename from ergo-nipopow/src/autolykos_pow_scheme.rs rename to ergo-chain-types/src/autolykos_pow_scheme.rs index 9ada5ca9f..3c69271ea 100644 --- a/ergo-nipopow/src/autolykos_pow_scheme.rs +++ b/ergo-chain-types/src/autolykos_pow_scheme.rs @@ -1,22 +1,95 @@ +//! Autolykos Pow puzzle scheme implementation +//! +//! See for reference implmentation - +//! +//! Based on k-sum problem, so general idea is to find k numbers in a table of size N, such that +//! sum of numbers (or a hash of the sum) is less than target value. +//! +//! See for details +//! +//! CPU Mining process is implemented in inefficient way and should not be used in real environment. +//! +//! See for full description +use alloc::boxed::Box; +use alloc::vec; +use alloc::vec::Vec; use bounded_integer::{BoundedI32, BoundedU64}; use derive_more::From; -use ergo_chain_types::Header; +use k256::{elliptic_curve::PrimeField, Scalar}; use num_bigint::{BigInt, Sign}; +use num_traits::Num; use sigma_ser::ScorexSerializationError; use sigma_util::hash::blake2b256_hash; +use thiserror::Error; -/// Autolykos PoW puzzle scheme implementation. -/// -/// See for reference implmentation - -/// -/// Based on k-sum problem, so general idea is to find k numbers in a table of size N, such that -/// sum of numbers (or a hash of the sum) is less than target value. +use crate::Header; + +/// The "compact" format is an encoding of a whole number `N` using an unsigned 32 bit number. +/// This number encodes a base-256 scientific notation representation of `N` (similar to a floating +/// point format): +/// - The most significant 8 bits represent the number of bytes necessary to represent `N` in +/// two's-complement form; denote it by `exponent`. +/// - The lower 23 bits are the mantissa(significand). +/// - Bit number 24 (0x800000) represents the sign of N. /// -/// See for details +/// There are 2 cases to consider: +/// - If `exponent >= 3` then `N` is represented by +/// `(-1^sign) * mantissa * 256^(exponent-3)` +/// E.g. suppose the compact form is given in hex-format by `0x04123456`. Mantissa is `0x123456` +/// and `exponent == 4`. So `N == 0x123456 * 265^1`. Now note that we need exactly 4 bytes to +/// represent `N`; 3 bytes for the mantissa and 1 byte for the rest. In base-256: +/// `N == B(0x12)B(0x34)B(0x56)0` +/// where `B(y)` denotes the base-256 representation of a hex-number `y` (note how each base-256 +/// digit is represented by a single-byte). +/// - If `exponent < 3` then `N` is represented by the `exponent`-most-significant-bytes of the +/// mantissa. E.g. suppose the compact form is given in hex-format by `0x01003456`. Noting that +/// each hex-digit is represented by 4-bits, our `exponent == 0x01` which is `1` base-10. The +/// mantissa is represented by `0x003456` and it's most signficant byte is `0x00`. Therefore +/// `N == 0`. /// -/// CPU Mining process is implemented in inefficient way and should not be used in real environment. +/// Satoshi's original implementation used BN_bn2mpi() and BN_mpi2bn(). MPI uses the most +/// significant bit of the first byte as sign. Thus 0x1234560000 is compact 0x05123456 and +/// 0xc0de000000 is compact 0x0600c0de. Compact 0x05c0de00 would be -0x40de000000. /// -/// See for full description +/// Bitcoin only uses this "compact" format for encoding difficulty targets, which are unsigned +/// 256bit quantities. Thus, all the complexities of the sign bit and using base 256 are probably +/// an implementation accident. +pub fn decode_compact_bits(n_bits: u64) -> BigInt { + let compact = n_bits as i64; + let size = ((compact >> 24) as i32) & 0xFF; + if size == 0 { + return BigInt::from(0); + } + let mut buf: Vec = vec![0; size as usize]; + if size >= 1 { + // Store the first byte of the mantissa + buf[0] = (((compact >> 16) as i32) & 0xFF) as i8; + } + if size >= 2 { + buf[1] = (((compact >> 8) as i32) & 0xFF) as i8; + } + if size >= 3 { + buf[2] = ((compact as i32) & 0xFF) as i8; + } + + let is_negative = (buf[0] as i32) & 0x80 == 0x80; + if is_negative { + buf[0] &= 0x7f; + let buf: Vec<_> = buf.into_iter().map(|x| x as u8).collect(); + -BigInt::from_signed_bytes_be(&buf) + } else { + let buf: Vec<_> = buf.into_iter().map(|x| x as u8).collect(); + BigInt::from_signed_bytes_be(&buf) + } +} + +/// Order of the secp256k1 elliptic curve as BigInt +pub fn order_bigint() -> BigInt { + #[allow(clippy::unwrap_used)] + BigInt::from_str_radix(Scalar::MODULUS, 16).unwrap() +} + +/// Autolykos PoW puzzle scheme implementation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AutolykosPowScheme { /// Represents the number of elements in one solution. **Important assumption**: `k <= 32`. @@ -170,30 +243,36 @@ fn as_unsigned_byte_array( let start = usize::from(bytes[0] == 0); let count = bytes.len() - start; if count > length { - return Err(AutolykosPowSchemeError::BigIntToFixedByteArrayError); + return Err(AutolykosPowSchemeError::BigIntToByteArrayError); } - let mut res: Vec<_> = std::iter::repeat(0).take(length).collect(); + let mut res: Vec<_> = vec![0; length]; res[(length - count)..].copy_from_slice(&bytes[start..]); Ok(res) } -#[derive(PartialEq, Eq, Debug, Clone, From)] +/// Autolykos POW scheme error +#[derive(PartialEq, Eq, Debug, Clone, From, Error)] pub enum AutolykosPowSchemeError { /// Scorex-serialization error + #[error("Scorex serialization error: {0}")] ScorexSerializationError(ScorexSerializationError), /// Error occurring when trying to convert a `BigInt` into a fixed-length byte-array. - BigIntToFixedByteArrayError, + #[error("Error converting BigInt to byte array")] + BigIntToByteArrayError, /// Occurs when `Header.version == 1` and the `pow_distance` parameter is None. + #[error("PoW distance not found for Autolykos1 Header")] MissingPowDistanceParameter, + /// Checking proof-of-work for AutolykosV1 is not supported + #[error("Header.check_pow is not supported for Autolykos1")] + Unsupported, } /// The following tests are taken from #[allow(clippy::unwrap_used)] #[cfg(test)] mod tests { - use ergotree_ir::{serialization::SigmaSerializable, sigma_protocol::dlog_group::order_bigint}; - - use crate::nipopow_algos::decode_compact_bits; + use num_bigint::ToBigInt; + use sigma_ser::ScorexSerializable; use super::*; @@ -288,7 +367,7 @@ mod tests { &header .autolykos_solution .miner_pk - .sigma_serialize_bytes() + .scorex_serialize_bytes() .unwrap() ), "03bedaee069ff4829500b3c07c4d5fe6b3ea3d3bf76c5c28c1d4dcdb1bed0ade0c" @@ -317,4 +396,47 @@ mod tests { assert!(hit >= target_b); } + + #[test] + fn test_decode_n_bits() { + // Following example taken from https://btcinformation.org/en/developer-reference#target-nbits + let n_bits = 0x181bc330; + assert_eq!( + decode_compact_bits(n_bits), + BigInt::parse_bytes(b"1bc330000000000000000000000000000000000000000000", 16).unwrap() + ); + + let n_bits = 0x01003456; + assert_eq!( + decode_compact_bits(n_bits), + ToBigInt::to_bigint(&0x00).unwrap() + ); + + let n_bits = 0x01123456; + assert_eq!( + decode_compact_bits(n_bits), + ToBigInt::to_bigint(&0x12).unwrap() + ); + + let n_bits = 0x04923456; + assert_eq!( + decode_compact_bits(n_bits), + ToBigInt::to_bigint(&-0x12345600).unwrap() + ); + + let n_bits = 0x04123456; + assert_eq!( + decode_compact_bits(n_bits), + ToBigInt::to_bigint(&0x12345600).unwrap() + ); + + let n_bits = 0x05123456; + assert_eq!( + decode_compact_bits(n_bits), + ToBigInt::to_bigint(&0x1234560000i64).unwrap() + ); + + let n_bits = 16842752; + assert_eq!(decode_compact_bits(n_bits), BigInt::from(1_u8)); + } } diff --git a/ergo-chain-types/src/header.rs b/ergo-chain-types/src/header.rs index 32befab68..51b36f717 100644 --- a/ergo-chain-types/src/header.rs +++ b/ergo-chain-types/src/header.rs @@ -1,4 +1,7 @@ //! Block header +use crate::autolykos_pow_scheme::{ + decode_compact_bits, order_bigint, AutolykosPowScheme, AutolykosPowSchemeError, +}; use crate::{ADDigest, BlockId, Digest32, EcPoint}; use alloc::boxed::Box; use alloc::vec::Vec; @@ -83,6 +86,16 @@ impl Header { } Ok(data) } + /// Check that proof of work was valid for header. Only Autolykos2 is supported + pub fn check_pow(&self) -> Result { + if self.version != 1 { + let hit = AutolykosPowScheme::default().pow_hit(self)?; + let target = order_bigint() / decode_compact_bits(self.n_bits); + Ok(hit < target) + } else { + Err(AutolykosPowSchemeError::Unsupported) + } + } } impl ScorexSerializable for Header { @@ -419,6 +432,7 @@ mod tests { }"#; let header: Header = serde_json::from_str(json).unwrap(); assert_eq!(header.height, 471746); + assert!(header.check_pow().unwrap()); assert_eq!( header.autolykos_solution.pow_distance, Some(BigInt::from_str( @@ -457,6 +471,7 @@ mod tests { }"#; let header: Header = serde_json::from_str(json).unwrap(); assert_eq!(header.height, 471746); + assert!(header.check_pow().unwrap()); assert_eq!( header.autolykos_solution.pow_distance, Some(BigInt::from_str( diff --git a/ergo-chain-types/src/lib.rs b/ergo-chain-types/src/lib.rs index f6fe60f5b..719be1383 100644 --- a/ergo-chain-types/src/lib.rs +++ b/ergo-chain-types/src/lib.rs @@ -22,6 +22,7 @@ #[macro_use] extern crate alloc; +pub mod autolykos_pow_scheme; mod base16_bytes; mod block_id; mod digest32; diff --git a/ergo-nipopow/src/lib.rs b/ergo-nipopow/src/lib.rs index 68f7e5a62..caa76063a 100644 --- a/ergo-nipopow/src/lib.rs +++ b/ergo-nipopow/src/lib.rs @@ -18,11 +18,10 @@ #![deny(clippy::unimplemented)] #![deny(clippy::panic)] -mod autolykos_pow_scheme; mod nipopow_algos; mod nipopow_proof; mod nipopow_verifier; -pub use nipopow_algos::{decode_compact_bits, NipopowAlgos, INTERLINK_VECTOR_PREFIX}; +pub use nipopow_algos::{NipopowAlgos, INTERLINK_VECTOR_PREFIX}; pub use nipopow_proof::{NipopowProof, NipopowProofError, PoPowHeader}; pub use nipopow_verifier::NipopowVerifier; diff --git a/ergo-nipopow/src/nipopow_algos.rs b/ergo-nipopow/src/nipopow_algos.rs index 417d57b9e..48fa5b207 100644 --- a/ergo-nipopow/src/nipopow_algos.rs +++ b/ergo-nipopow/src/nipopow_algos.rs @@ -1,14 +1,13 @@ -use ergo_chain_types::Header; -use ergotree_ir::sigma_protocol::dlog_group::order_bigint; -use num_bigint::BigInt; +use ergo_chain_types::{ + autolykos_pow_scheme::{ + decode_compact_bits, order_bigint, AutolykosPowScheme, AutolykosPowSchemeError, + }, + Header, +}; use num_traits::ToPrimitive; use std::convert::TryInto; -use crate::{ - autolykos_pow_scheme::{AutolykosPowScheme, AutolykosPowSchemeError}, - nipopow_proof::PoPowHeader, - NipopowProof, NipopowProofError, -}; +use crate::{nipopow_proof::PoPowHeader, NipopowProof, NipopowProofError}; use ergo_chain_types::{BlockId, Digest32, ExtensionCandidate}; /// Prefix for Block Interlinks @@ -349,112 +348,3 @@ fn extension_merkletree(kv: &[([u8; 2], Vec)]) -> ergo_merkle_tree::MerkleTr .collect::>(); ergo_merkle_tree::MerkleTree::new(leafs) } - -/// The "compact" format is an encoding of a whole number `N` using an unsigned 32 bit number. -/// This number encodes a base-256 scientific notation representation of `N` (similar to a floating -/// point format): -/// - The most significant 8 bits represent the number of bytes necessary to represent `N` in -/// two's-complement form; denote it by `exponent`. -/// - The lower 23 bits are the mantissa(significand). -/// - Bit number 24 (0x800000) represents the sign of N. -/// -/// There are 2 cases to consider: -/// - If `exponent >= 3` then `N` is represented by -/// `(-1^sign) * mantissa * 256^(exponent-3)` -/// E.g. suppose the compact form is given in hex-format by `0x04123456`. Mantissa is `0x123456` -/// and `exponent == 4`. So `N == 0x123456 * 265^1`. Now note that we need exactly 4 bytes to -/// represent `N`; 3 bytes for the mantissa and 1 byte for the rest. In base-256: -/// `N == B(0x12)B(0x34)B(0x56)0` -/// where `B(y)` denotes the base-256 representation of a hex-number `y` (note how each base-256 -/// digit is represented by a single-byte). -/// - If `exponent < 3` then `N` is represented by the `exponent`-most-significant-bytes of the -/// mantissa. E.g. suppose the compact form is given in hex-format by `0x01003456`. Noting that -/// each hex-digit is represented by 4-bits, our `exponent == 0x01` which is `1` base-10. The -/// mantissa is represented by `0x003456` and it's most signficant byte is `0x00`. Therefore -/// `N == 0`. -/// -/// Satoshi's original implementation used BN_bn2mpi() and BN_mpi2bn(). MPI uses the most -/// significant bit of the first byte as sign. Thus 0x1234560000 is compact 0x05123456 and -/// 0xc0de000000 is compact 0x0600c0de. Compact 0x05c0de00 would be -0x40de000000. -/// -/// Bitcoin only uses this "compact" format for encoding difficulty targets, which are unsigned -/// 256bit quantities. Thus, all the complexities of the sign bit and using base 256 are probably -/// an implementation accident. -pub fn decode_compact_bits(n_bits: u64) -> BigInt { - let compact = n_bits as i64; - let size = ((compact >> 24) as i32) & 0xFF; - if size == 0 { - return BigInt::from(0); - } - let mut buf: Vec = std::iter::repeat(0).take(size as usize).collect(); - if size >= 1 { - // Store the first byte of the mantissa - buf[0] = (((compact >> 16) as i32) & 0xFF) as i8; - } - if size >= 2 { - buf[1] = (((compact >> 8) as i32) & 0xFF) as i8; - } - if size >= 3 { - buf[2] = ((compact as i32) & 0xFF) as i8; - } - - let is_negative = (buf[0] as i32) & 0x80 == 0x80; - if is_negative { - buf[0] &= 0x7f; - let buf: Vec<_> = buf.into_iter().map(|x| x as u8).collect(); - -BigInt::from_signed_bytes_be(&buf) - } else { - let buf: Vec<_> = buf.into_iter().map(|x| x as u8).collect(); - BigInt::from_signed_bytes_be(&buf) - } -} - -#[allow(clippy::unwrap_used)] -#[cfg(test)] -mod tests { - use super::*; - use num_bigint::ToBigInt; - - #[test] - fn test_decode_n_bits() { - // Following example taken from https://btcinformation.org/en/developer-reference#target-nbits - let n_bits = 0x181bc330; - assert_eq!( - decode_compact_bits(n_bits), - BigInt::parse_bytes(b"1bc330000000000000000000000000000000000000000000", 16).unwrap() - ); - - let n_bits = 0x01003456; - assert_eq!( - decode_compact_bits(n_bits), - ToBigInt::to_bigint(&0x00).unwrap() - ); - - let n_bits = 0x01123456; - assert_eq!( - decode_compact_bits(n_bits), - ToBigInt::to_bigint(&0x12).unwrap() - ); - - let n_bits = 0x04923456; - assert_eq!( - decode_compact_bits(n_bits), - ToBigInt::to_bigint(&-0x12345600).unwrap() - ); - - let n_bits = 0x04123456; - assert_eq!( - decode_compact_bits(n_bits), - ToBigInt::to_bigint(&0x12345600).unwrap() - ); - - let n_bits = 0x05123456; - assert_eq!( - decode_compact_bits(n_bits), - ToBigInt::to_bigint(&0x1234560000i64).unwrap() - ); - - let n_bits = 16842752; - assert_eq!(decode_compact_bits(n_bits), BigInt::from(1_u8)); - } -} diff --git a/ergo-nipopow/src/nipopow_proof.rs b/ergo-nipopow/src/nipopow_proof.rs index 7ed0ca3b9..d10fbac11 100644 --- a/ergo-nipopow/src/nipopow_proof.rs +++ b/ergo-nipopow/src/nipopow_proof.rs @@ -1,5 +1,5 @@ use derive_more::From; -use ergo_chain_types::{BlockId, Header}; +use ergo_chain_types::{autolykos_pow_scheme::AutolykosPowSchemeError, BlockId, Header}; use ergo_merkle_tree::BatchMerkleProof; use serde::{Deserialize, Serialize}; use sigma_ser::{ @@ -7,7 +7,7 @@ use sigma_ser::{ ScorexParsingError, ScorexSerializable, ScorexSerializeResult, }; -use crate::{autolykos_pow_scheme, nipopow_algos::NipopowAlgos}; +use crate::nipopow_algos::NipopowAlgos; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] /// A structure representing NiPoPow proof as a persistent modifier. @@ -189,7 +189,7 @@ impl ScorexSerializable for NipopowProof { pub enum NipopowProofError { /// Errors from `AutolykosPowScheme` #[error("{0:?}")] - AutolykosPowSchemeError(autolykos_pow_scheme::AutolykosPowSchemeError), + AutolykosPowSchemeError(AutolykosPowSchemeError), /// `k` parameter == 0. Must be >= 1. #[error("k parameter == 0. Must be >= 1")] ZeroKParameter, @@ -288,7 +288,7 @@ impl ScorexSerializable for PoPowHeader { #[cfg(feature = "arbitrary")] #[allow(clippy::unwrap_used)] mod arbitrary { - use crate::autolykos_pow_scheme::AutolykosPowScheme; + use ergo_chain_types::autolykos_pow_scheme::AutolykosPowScheme; use super::*; use ergo_chain_types::Digest32; diff --git a/ergotree-interpreter/src/eval.rs b/ergotree-interpreter/src/eval.rs index 1cc40047a..de7ddefbb 100644 --- a/ergotree-interpreter/src/eval.rs +++ b/ergotree-interpreter/src/eval.rs @@ -319,6 +319,7 @@ fn smethod_eval_fn(method: &SMethod) -> Result { sheader::POW_DISTANCE_METHOD_ID => self::sheader::POW_DISTANCE_EVAL_FN, sheader::POW_NONCE_METHOD_ID => self::sheader::POW_NONCE_EVAL_FN, sheader::VOTES_METHOD_ID => self::sheader::VOTES_EVAL_FN, + sheader::CHECK_POW_METHOD_ID => self::sheader::CHECK_POW_EVAL_FN, method_id => { return Err(EvalError::NotFound(format!( "Eval fn: method {:?} with method id {:?} not found in SHeader", diff --git a/ergotree-interpreter/src/eval/error.rs b/ergotree-interpreter/src/eval/error.rs index 464736da6..58a4e2fdb 100644 --- a/ergotree-interpreter/src/eval/error.rs +++ b/ergotree-interpreter/src/eval/error.rs @@ -2,6 +2,7 @@ use alloc::boxed::Box; use alloc::string::String; use core::fmt::Debug; use core::fmt::Display; +use ergo_chain_types::autolykos_pow_scheme::AutolykosPowSchemeError; use ergotree_ir::ergo_tree::ErgoTreeVersion; use ergotree_ir::mir::expr::SubstDeserializeError; @@ -87,6 +88,9 @@ pub enum EvalError { /// Deserialize substitution error, see [`ergotree_ir::mir::expr::Expr::substitute_deserialize`] #[error("DeserializeRegister/DeserializeContext error: {0}")] SubstDeserializeError(#[from] SubstDeserializeError), + /// Autolykos PoW error + #[error("Autolykos PoW error: {0}")] + AutolykosPowSchemeError(#[from] AutolykosPowSchemeError), } /// Wrapped error with source span diff --git a/ergotree-interpreter/src/eval/sheader.rs b/ergotree-interpreter/src/eval/sheader.rs index 7253c6bf3..117c06b40 100644 --- a/ergotree-interpreter/src/eval/sheader.rs +++ b/ergotree-interpreter/src/eval/sheader.rs @@ -5,7 +5,10 @@ use core::convert::TryInto; use alloc::vec::Vec; use ergo_chain_types::Header; -use ergotree_ir::{bigint256::BigInt256, mir::constant::TryExtractInto}; +use ergotree_ir::{ + bigint256::BigInt256, + mir::{constant::TryExtractInto, value::Value}, +}; use super::{EvalError, EvalFn}; @@ -90,6 +93,14 @@ pub(crate) static VOTES_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| { Ok(Into::>::into(header.votes).into()) }; +pub(crate) static CHECK_POW_EVAL_FN: EvalFn = |_mc, _env, _ctx, obj, _args| match obj { + Value::Header(header) => Ok(header.check_pow()?.into()), + _ => Err(EvalError::UnexpectedValue(format!( + "SHeader.checkpow expected obj to be Value::Global, got {:?}", + obj + ))), +}; + #[cfg(test)] #[cfg(feature = "arbitrary")] #[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] @@ -101,8 +112,15 @@ mod tests { use ergotree_ir::{ bigint256::BigInt256, chain::context::Context, - mir::{coll_by_index::ByIndex, expr::Expr, property_call::PropertyCall}, - types::{scontext, sheader, smethod::SMethod}, + mir::{ + coll_by_index::ByIndex, expr::Expr, method_call::MethodCall, + property_call::PropertyCall, + }, + types::{ + scontext::{self, HEADERS_PROPERTY}, + sheader, + smethod::SMethod, + }, }; use sigma_test_util::force_any_val; use sigma_util::AsVecU8; @@ -343,4 +361,47 @@ mod tests { let expr = create_get_header_property_expr(unknown_property); assert!(try_eval_out_wo_ctx::(&expr).is_err()); } + #[test] + fn test_eval_check_pow() { + let mut ctx = force_any_val::(); + ctx.headers[0] = serde_json::from_str( + r#"{ + "extensionId": "d51a477cc12b187d9bc7f464b22d00e3aa7c92463874e863bf3acf2f427bb48b", + "difficulty": "1595361307131904", + "votes": "000000", + "timestamp": 1736177881102, + "size": 220, + "unparsedBytes": "", + "stateRoot": "4dfafb43842680fd5870d8204a218f873479e1f5da1b34b059ca8da526abcc8719", + "height": 1433531, + "nBits": 117811961, + "version": 3, + "id": "3473e7b5aaf623e4260d5798253d26f3cdc912c12594b7e3a979e3db8ed883f6", + "adProofsRoot": "73160faa9f0e47bf7da598d4e9d3de58e8a24b8564458ad8a4d926514f435dc1", + "transactionsRoot": "c88d5f50ece85c2b918b5bd41d2bc06159e6db1b3aad95091d994c836a172950", + "extensionHash": "d5a43bf63c1d8c7f10b15b6d2446abe565b93a4fd3f5ca785b00e6bda831644f", + "powSolutions": { + "pk": "0274e729bb6615cbda94d9d176a2f1525068f12b330e38bbbf387232797dfd891f", + "w": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "n": "a6905b8c65f5864a", + "d": 0 + }, + "adProofsId": "80a5ff0c6cd98440163bd27f2d7c775ea516af09024a98d9d83f16029bfbd034", + "transactionsId": "c7315c49df258522d3e92ce2653d9f4d8a35309a7a7dd470ebf8db53dd3fb792", + "parentId": "93172f3152a6a25dc89dc45ede1130c5eb86636a50bfb93a999556d16016ceb7" + }"#, + ) + .unwrap(); + // Add a mainnet block header with valid PoW to context. TODO: this can be simplified once Header serialization is added to sigma-rust (v6.0), right now we need to access CONTEXT.headers(0) + let headers = PropertyCall::new(Expr::Context, HEADERS_PROPERTY.clone()).unwrap(); + let header = ByIndex::new(headers.into(), 0i32.into(), None).unwrap(); + let check_pow: Expr = + MethodCall::new(header.into(), sheader::CHECK_POW_METHOD.clone(), vec![]) + .unwrap() + .into(); + assert!(eval_out::(&check_pow, &ctx)); + // Mutate header to invalidate proof-of-work + ctx.headers[0].timestamp -= 1; + assert!(!eval_out::(&check_pow, &ctx)); + } } diff --git a/ergotree-ir/src/sigma_protocol/dlog_group.rs b/ergotree-ir/src/sigma_protocol/dlog_group.rs index c339fb417..089383522 100644 --- a/ergotree-ir/src/sigma_protocol/dlog_group.rs +++ b/ergotree-ir/src/sigma_protocol/dlog_group.rs @@ -27,8 +27,6 @@ use bnum::BTryFrom; use elliptic_curve::rand_core::RngCore; use k256::elliptic_curve::PrimeField; use k256::Scalar; -use num_bigint::BigInt; -use num_traits::Num; use sigma_ser::ScorexSerializable; // /// Creates a random member of this Dlog group @@ -88,18 +86,11 @@ impl SigmaSerializable for ergo_chain_types::EcPoint { } } -const ORDER: &str = "+FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"; /// Order of the secp256k1 elliptic curve // Since secp256k1 order doesn't fit in a signed 256 bit integer, this returns an unsigned 256-bit integer instead pub fn order() -> U256 { #[allow(clippy::unwrap_used)] - U256::from_str_radix(ORDER, 16).unwrap() -} - -/// Order of the secp256k1 elliptic curve as BigInt -pub fn order_bigint() -> BigInt { - #[allow(clippy::unwrap_used)] - BigInt::from_str_radix(ORDER, 16).unwrap() + U256::from_str_radix(Scalar::MODULUS, 16).unwrap() } #[allow(clippy::unwrap_used)] diff --git a/ergotree-ir/src/types/sheader.rs b/ergotree-ir/src/types/sheader.rs index 22784ad3b..362cc84f0 100644 --- a/ergotree-ir/src/types/sheader.rs +++ b/ergotree-ir/src/types/sheader.rs @@ -1,10 +1,14 @@ #![allow(missing_docs)] +use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; use lazy_static::lazy_static; +use crate::ergo_tree::ErgoTreeVersion; use crate::serialization::types::TypeCode; +use crate::types::sfunc::SFunc; +use crate::types::stype_companion::STypeCompanion; use super::smethod::{MethodId, SMethod, SMethodDesc}; use super::stype::SType::{self, SByte, SColl}; @@ -44,6 +48,8 @@ pub const POW_NONCE_METHOD_ID: MethodId = MethodId(13); pub const POW_DISTANCE_METHOD_ID: MethodId = MethodId(14); /// `Header.votes` pub const VOTES_METHOD_ID: MethodId = MethodId(15); +/// `Header.checkPow` +pub const CHECK_POW_METHOD_ID: MethodId = MethodId(16); lazy_static! { /// Header method descriptors @@ -64,6 +70,7 @@ lazy_static! { &POW_NONCE_PROPERTY_METHOD_DESC, &POW_DISTANCE_PROPERTY_METHOD_DESC, &VOTES_PROPERTY_METHOD_DESC, + &CHECK_POW_METHOD_DESC ] ; } @@ -154,6 +161,19 @@ lazy_static! { ); static ref VOTES_PROPERTY_METHOD_DESC: SMethodDesc = property("votes", SColl(SByte.into()), VOTES_METHOD_ID); + static ref CHECK_POW_METHOD_DESC: SMethodDesc = SMethodDesc { + method_id: CHECK_POW_METHOD_ID, + name: "checkPow", + tpe: SFunc { + t_dom: vec![SType::SHeader], + t_range: Box::new(SType::SBoolean), + tpe_params: vec![] + }, + explicit_type_args: vec![], + min_version: ErgoTreeVersion::V3 + }; + /// Header.checkPow + pub static ref CHECK_POW_METHOD: SMethod = SMethod::new(STypeCompanion::Header, CHECK_POW_METHOD_DESC.clone()); } fn property(name: &'static str, res_tpe: SType, id: MethodId) -> SMethodDesc {