Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b2587c5
ergo-nipopow: tolerate skipped prefix entries in has_valid_connections
mwaddip Apr 9, 2026
fded795
test(nipopow): add regression test for pack_interlinks key encoding
mwaddip May 19, 2026
8b82375
fix(nipopow): pack_interlinks key encoding — use first-occurrence pos…
mwaddip May 19, 2026
e9214ec
fix: reject non-pair tuples at eval to match sigma-state consensus
mwaddip May 30, 2026
57570df
fix(ergotree-ir): Global.powHit returns UnsignedBigInt, not Boolean
mwaddip Jun 4, 2026
d64843f
fix(ergo-lib): checked ERG summation in stateful transaction validation
mwaddip Jun 7, 2026
bd608a6
fix: Option DATA parser treats any nonzero tag as Some, per JVM getOp…
mwaddip Jun 7, 2026
96ba642
version 0.30.0 set
kushti Jun 9, 2026
c489b4d
fix: reject self ContextExtension keys >= 0x80 at reduction
mwaddip Jun 13, 2026
681225c
fix(ergo-chain-types): read Header version as signed at unparsedBytes…
mwaddip Jun 15, 2026
e9ab57e
Merge pull request #895 from mwaddip/fix/option-data-nonzero-tag
kushti Jul 9, 2026
15c62d2
Merge pull request #909 from mwaddip/fix/header-version-signed-gate
kushti Jul 9, 2026
19c44e0
Merge pull request #906 from mwaddip/fix/context-extension-key-domain
kushti Jul 9, 2026
b8b3b0b
Merge pull request #868 from mwaddip/fix/tuple-arity-jvm-parity
kushti Jul 9, 2026
fd51c19
Merge pull request #877 from mwaddip/fix/powhit-return-type
kushti Jul 9, 2026
9b17c7a
fix: reject unsupported (arity != 2) tuple-typed values at eval (JVM …
mwaddip Jun 8, 2026
12c1acb
check_value_type before eval (abort earlier per kushti review)
mwaddip Jun 23, 2026
92bad43
Merge pull request #897 from mwaddip/fix/checktype-ingress-divergences
kushti Jul 9, 2026
be76e1c
Merge pull request #891 from mwaddip/fix/erg-sum-overflow
kushti Jul 15, 2026
a0dfca3
fix(nipopow): validate initial proof
a-shannon Jul 31, 2026
5cbb5e1
Merge pull request #852 from mwaddip/fix/nipopow-prefix-connection-lo…
kushti Aug 3, 2026
cc4cef3
Merge pull request #866 from mwaddip/fix/nipopow-pack-interlinks-jvm-…
kushti Aug 3, 2026
3d53c36
Merge pull request #914 from a-shannon/fix/nipopow-validate-initial-p…
kushti Aug 3, 2026
df3f16b
style: allow clippy::expect_used at the two pack_interlinks index casts
mwaddip Aug 3, 2026
aebee7e
style: rustfmt ergo-nipopow/src/nipopow_proof.rs
mwaddip Aug 3, 2026
4792b84
Merge pull request #915 from mwaddip/style/v030-fmt-clippy
kushti Aug 3, 2026
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
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ edition = "2021"
[workspace.dependencies]
sigma-util = { version = "^0.18.0", path = "./sigma-util" }
sigma-ser = { version = "^0.19.0", path = "./sigma-ser", default-features = false }
ergotree-ir = { version = "^0.28.0", path = "./ergotree-ir", default-features = false }
ergotree-ir = { version = "^0.30.0", path = "./ergotree-ir", default-features = false }
ergo-chain-types = { version = "^0.15.0", path = "./ergo-chain-types", default-features = false }
sigma-test-util = { version = "^0.3.0", path = "./sigma-test-util" }
ergoscript-compiler = { version = "^0.24.0", path = "./ergoscript-compiler" }
ergotree-interpreter = { version = "^0.28.0", path = "./ergotree-interpreter", default-features = false }
ergotree-interpreter = { version = "^0.30.0", path = "./ergotree-interpreter", default-features = false }
ergo-nipopow = { version = "^0.15", path = "./ergo-nipopow" }
ergo-merkle-tree = { version = "^0.15.0", path = "./ergo-merkle-tree", default-features = false }
ergo-rest = { version = "^0.13.0", path = "./ergo-rest" }
ergo-lib = { version = "^0.28.0", path = "./ergo-lib" }
ergo-lib = { version = "^0.30.0", path = "./ergo-lib" }
k256 = { version = "0.13.1", default-features = false, features = [
"arithmetic",
"ecdsa",
Expand Down
2 changes: 1 addition & 1 deletion bindings/ergo-lib-c-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ergo-lib-c-core"
version = "0.28.0"
version = "0.30.0"
license = "CC0-1.0"
authors = ["Denys Zadorozhnyi <denys@zadorozhnyi.com>"]
repository.workspace = true
Expand Down
4 changes: 2 additions & 2 deletions bindings/ergo-lib-c/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ergo-lib-c"
version = "0.28.0"
version = "0.30.0"
license = "CC0-1.0"
authors = ["Denys Zadorozhnyi <denys@zadorozhnyi.com>"]
repository.workspace = true
Expand All @@ -14,7 +14,7 @@ name = "ergo"
crate-type = ["staticlib"]

[dependencies]
ergo-lib-c-core = { version = "^0.28.0", path = "../ergo-lib-c-core" }
ergo-lib-c-core = { version = "^0.30.0", path = "../ergo-lib-c-core" }
paste = "^1.0"

[features]
Expand Down
4 changes: 2 additions & 2 deletions bindings/ergo-lib-jni/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ergo-lib-jni"
version = "0.28.0"
version = "0.30.0"
license = "CC0-1.0"
authors = ["Denys Zadorozhnyi <denys@zadorozhnyi.com>"]
repository.workspace = true
Expand All @@ -16,4 +16,4 @@ path = "src/main/rust/lib.rs"
jni = "0.19.0"
log = "0.4.16"
ergo-lib = { workspace = true }
ergo-lib-c-core = { version = "^0.28.0", path = "../ergo-lib-c-core" }
ergo-lib-c-core = { version = "^0.30.0", path = "../ergo-lib-c-core" }
2 changes: 1 addition & 1 deletion bindings/ergo-lib-python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ergo-lib-python"
version = "0.28.0"
version = "0.30.0"
license = "CC0-1.0"
authors = ["Kamal Ahmad <kamalahmad22@pm.me>"]
edition.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion bindings/ergo-lib-wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ergo-lib-wasm"
version = "0.28.0"
version = "0.30.0"
license = "CC0-1.0"
authors = ["Denys Zadorozhnyi <denys@zadorozhnyi.com>"]
repository = "https://github.com/ergoplatform/sigma-rust"
Expand Down
187 changes: 186 additions & 1 deletion ergo-chain-generation/src/fake_pow_scheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#[cfg(test)]
mod tests {
use ergo_lib::ergo_chain_types::{blake2b256_hash, ADDigest, BlockId, Digest32};
use ergo_nipopow::{NipopowAlgos, NipopowProof};
use ergo_nipopow::{NipopowAlgos, NipopowProof, NipopowVerifier, INTERLINK_VECTOR_PREFIX};

use ergo_chain_types::{autolykos_pow_scheme::order_bigint, AutolykosSolution, Header, Votes};
use ergo_lib::ergotree_interpreter::sigma_protocol::private_input::DlogProverInput;
Expand Down Expand Up @@ -179,6 +179,191 @@ mod tests {
}
}

fn generated_nipopow_proof() -> (BlockId, NipopowProof) {
let m = 5;
let k = 5;
let chain = generate_popowheader_chain(100, None);
let genesis_id = chain[0].header.id;
let proof = NipopowAlgos::default().prove(&chain, k, m).unwrap();
assert_eq!(proof.prefix[0].header.id, genesis_id);
(genesis_id, proof)
}

fn assert_initial_proof_ignored(genesis_id: BlockId, proof: NipopowProof) {
let mut verifier = NipopowVerifier::new(genesis_id);
assert!(verifier.process(proof).is_ok());
assert!(verifier.best_proof().is_none());
assert!(verifier.best_chain().is_empty());
}

fn indexed_block_id(index: usize) -> BlockId {
let mut bytes = [0u8; 32];
bytes[..2].copy_from_slice(&u16::try_from(index).unwrap().to_be_bytes());
BlockId(Digest32::from(bytes))
}

fn set_first_prefix_interlinks_with_valid_proof(
proof: &mut NipopowProof,
interlinks: Vec<BlockId>,
) {
let extension =
ExtensionCandidate::new(NipopowAlgos::pack_interlinks(interlinks.clone())).unwrap();
let interlinks_proof = NipopowAlgos::proof_for_interlink_vector(&extension).unwrap();
proof.prefix[0].interlinks = interlinks;
proof.prefix[0].interlinks_proof = interlinks_proof;
}

#[test]
fn test_nipopow_verifier_accepts_valid_initial_proof() {
let (genesis_id, proof) = generated_nipopow_proof();
let mut verifier = NipopowVerifier::new(genesis_id);
assert!(verifier.process(proof.clone()).is_ok());
assert_eq!(verifier.best_proof(), Some(proof));
}

#[test]
fn test_nipopow_verifier_ignores_wrong_genesis() {
let (_, proof) = generated_nipopow_proof();
let wrong_genesis = BlockId(Digest32::zero());
assert_ne!(proof.prefix[0].header.id, wrong_genesis);
assert_initial_proof_ignored(wrong_genesis, proof);
}

#[test]
fn test_nipopow_verifier_rejects_disconnected_initial_proof() {
let (genesis_id, mut proof) = generated_nipopow_proof();
assert!(!proof.suffix_tail.is_empty());
proof.suffix_tail[0].parent_id = BlockId(Digest32::zero());
assert!(!proof.has_valid_connections());
assert_initial_proof_ignored(genesis_id, proof);
}

#[test]
fn test_nipopow_verifier_rejects_non_increasing_initial_proof() {
let (genesis_id, mut proof) = generated_nipopow_proof();
assert!(!proof.suffix_tail.is_empty());
proof.suffix_tail[0].height = proof.suffix_head.header.height;
assert!(proof.has_valid_connections());
assert_initial_proof_ignored(genesis_id, proof);
}

#[test]
fn test_nipopow_verifier_rejects_invalid_interlinks_initial_proof() {
let (genesis_id, mut proof) = generated_nipopow_proof();
assert!(proof.suffix_head.check_interlinks_proof());
proof.suffix_head.interlinks.push(BlockId(Digest32::zero()));
assert!(!proof.suffix_head.check_interlinks_proof());
assert!(proof.has_valid_connections());
assert_initial_proof_ignored(genesis_id, proof);
}

#[test]
fn test_nipopow_verifier_rejects_empty_interlinks_with_nonempty_proof() {
let (genesis_id, mut proof) = generated_nipopow_proof();
let extension =
ExtensionCandidate::new(NipopowAlgos::pack_interlinks(vec![genesis_id])).unwrap();
let nonempty_proof = NipopowAlgos::proof_for_interlink_vector(&extension).unwrap();
assert!(!nonempty_proof.get_indices().is_empty());
proof.prefix[0].interlinks.clear();
proof.prefix[0].interlinks_proof = nonempty_proof;
assert!(proof.has_valid_connections());
assert_initial_proof_ignored(genesis_id, proof);
}

#[test]
fn test_nipopow_verifier_rejects_interlink_run_overflow() {
let (genesis_id, mut proof) = generated_nipopow_proof();
proof.prefix[0].interlinks = vec![genesis_id; usize::from(u8::MAX) + 1];
assert!(proof.has_valid_connections());
assert_initial_proof_ignored(genesis_id, proof);
}

#[test]
fn test_nipopow_verifier_rejects_interlink_key_overflow() {
let (genesis_id, mut proof) = generated_nipopow_proof();
proof.prefix[0].interlinks = (0..(usize::from(u8::MAX) + 2))
.map(indexed_block_id)
.collect();
assert!(proof.has_valid_connections());
assert_initial_proof_ignored(genesis_id, proof);
}

#[test]
fn test_nipopow_verifier_rejects_interlink_key_position_overflow() {
let (genesis_id, mut proof) = generated_nipopow_proof();
let second_run_id = indexed_block_id(1);
let third_run_id = indexed_block_id(2);
assert_ne!(genesis_id, second_run_id);
assert_ne!(genesis_id, third_run_id);
assert_ne!(second_run_id, third_run_id);
let mut interlinks = vec![genesis_id; usize::from(u8::MAX)];
interlinks.extend([second_run_id, third_run_id]);
let interlink_field_value = |count: u8, block_id: BlockId| {
let mut value = vec![count];
let block_id_bytes: Vec<u8> = block_id.0.into();
value.extend(block_id_bytes);
value
};
let extension = ExtensionCandidate::new(vec![
(
[INTERLINK_VECTOR_PREFIX, 0],
interlink_field_value(u8::MAX, genesis_id),
),
(
[INTERLINK_VECTOR_PREFIX, 1],
interlink_field_value(1, second_run_id),
),
(
[INTERLINK_VECTOR_PREFIX, 2],
interlink_field_value(1, third_run_id),
),
])
.unwrap();
proof.prefix[0].interlinks = interlinks;
proof.prefix[0].interlinks_proof =
NipopowAlgos::proof_for_interlink_vector(&extension).unwrap();
assert!(proof.has_valid_connections());
assert_initial_proof_ignored(genesis_id, proof);
}

#[test]
fn test_nipopow_verifier_accepts_maximum_interlink_run() {
let (genesis_id, mut proof) = generated_nipopow_proof();
set_first_prefix_interlinks_with_valid_proof(
&mut proof,
vec![genesis_id; usize::from(u8::MAX)],
);
assert!(proof.prefix[0].check_interlinks_proof());
let mut verifier = NipopowVerifier::new(genesis_id);
assert!(verifier.process(proof.clone()).is_ok());
assert_eq!(verifier.best_proof(), Some(proof));
}

#[test]
fn test_nipopow_verifier_accepts_maximum_interlink_key_count() {
let (genesis_id, mut proof) = generated_nipopow_proof();
let interlinks = (0..=usize::from(u8::MAX)).map(indexed_block_id).collect();
set_first_prefix_interlinks_with_valid_proof(&mut proof, interlinks);
assert!(proof.prefix[0].check_interlinks_proof());
let mut verifier = NipopowVerifier::new(genesis_id);
assert!(verifier.process(proof.clone()).is_ok());
assert_eq!(verifier.best_proof(), Some(proof));
}

#[test]
fn test_nipopow_verifier_accepts_valid_proof_after_invalid_initial_proof() {
let (genesis_id, valid_proof) = generated_nipopow_proof();
let mut invalid_proof = valid_proof.clone();
invalid_proof.suffix_tail[0].parent_id = BlockId(Digest32::zero());
assert!(!invalid_proof.has_valid_connections());

let mut verifier = NipopowVerifier::new(genesis_id);
assert!(verifier.process(invalid_proof).is_ok());
assert!(verifier.best_proof().is_none());
assert!(verifier.process(valid_proof.clone()).is_ok());
assert_eq!(verifier.best_proof(), Some(valid_proof));
}

#[test]
fn test_nipopow_lowest_common_ancestor_diverging() {
let popow_algos = NipopowAlgos::default();
Expand Down
51 changes: 48 additions & 3 deletions ergo-chain-types/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ impl Header {
w.write_all(&self.votes.0)?;

// For block version >= 2, this new byte encodes length of possible new fields.
// Set to 0 for now, so no new fields.
if self.version > 1 {
// Set to 0 for now, so no new fields. `version` is a signed Byte in the JVM,
// so a version >= 0x80 is negative and does not gate these fields -- compare signed.
if (self.version as i8) > 1 {
w.put_u8(self.unparsed_bytes.len().try_into()?)?;
w.write_all(&self.unparsed_bytes)?;
}
Expand Down Expand Up @@ -139,7 +140,9 @@ impl ScorexSerializable for Header {

// For block version >= 2, a new byte encodes length of possible new fields. If this byte >
// 0, we read new fields but do nothing, as semantics of the fields is not known.
let unparsed_bytes: Box<[u8]> = if version > 1 {
// `version` is a signed Byte in the JVM, so a version >= 0x80 is negative and skips
// this region (the AutolykosSolution parse then shifts accordingly) -- compare signed.
let unparsed_bytes: Box<[u8]> = if (version as i8) > 1 {
let new_field_size = r.get_u8()?;
if new_field_size > 0 {
let mut field_bytes: Vec<u8> = vec![0; new_field_size as usize];
Expand Down Expand Up @@ -508,3 +511,45 @@ mod tests {
);
}
}

#[allow(clippy::unwrap_used, clippy::panic)]
#[cfg(test)]
mod version_signedness_tests {
use crate::header::Header;
use sigma_ser::ScorexSerializable;

// SANTA ask 21 / V6-ARITY: the JVM (`HeaderWithoutPow`) reads `version` as a
// signed `Byte`, so a version byte 0x80 = -128 <= 1 SKIPS `unparsedBytes`,
// shifting the AutolykosSolution parse so `minerPk` reads as the infinity point.
// sigma-rust read `version` unsigned (128 > 1), consumed `unparsedBytes`, and
// parsed a different `minerPk` -- a deserialization fork vs sigma-state 6.0.3.
// The two witnesses are identical except the leading version byte.
const HEADER_V80: &str = "80ac2101807f0000ca01ff0119db227f202201007f62000177a080005d440896d05d3f80dcff7f5e7f59007294c180808d0158d1ff6ba10000f901c7f0ef87dcfff17fffacb6ff7f7f1180d2ff7f1e24ffffe1ff937f807f0797b9ff6ebdae007e5c8c00b8403d3701557181c8df800001b6d5009e2201c6ff807d71808c00019780f087adb3fcdbc0b3441480887f80007f4b01cf7f013ff1ffff564a0000b9a54f00770e807f41ff88c00240000080c0250000000003bedaee069ff4829500b3c07c4d5fe6b3ea3d3bf76c5c28c1d4dcdb1bed0ade0c0000000000003105";

fn miner_pk_hex(header_hex: &str) -> String {
let bytes = base16::decode(header_hex).unwrap();
let h = Header::scorex_parse_bytes(&bytes).unwrap();
base16::encode_lower(
&h.autolykos_solution
.miner_pk
.scorex_serialize_bytes()
.unwrap(),
)
}

#[test]
fn version_0x80_skips_unparsed_bytes_miner_pk_is_infinity() {
// signed -128 <= 1 -> skip unparsedBytes -> minerPk = point at infinity (33 zero bytes)
assert_eq!(miner_pk_hex(HEADER_V80), "00".repeat(33));
}

#[test]
fn version_0x7f_control_reads_unparsed_bytes_miner_pk_is_real() {
// 127 > 1 in both signed and unsigned readings -> reads unparsedBytes (fix does not change it)
let header_v7f = format!("7f{}", &HEADER_V80[2..]);
assert_eq!(
miner_pk_hex(&header_v7f),
"03bedaee069ff4829500b3c07c4d5fe6b3ea3d3bf76c5c28c1d4dcdb1bed0ade0c"
);
}
}
2 changes: 1 addition & 1 deletion ergo-lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ergo-lib"
version = "0.28.0"
version = "0.30.0"
license = "CC0-1.0"
authors = ["Denys Zadorozhnyi <denys@zadorozhnyi.com>"]
repository.workspace = true
Expand Down
Loading
Loading