Merge origin/testnet-prep (beed743) into testnet-adopt: testnet identity, fee floors and pgas table adopted 5 October 2026, G13 signed build inputs, release-0.3.6 plan
Owner's decision, 5 October 2026: the proposed testnet identity (docs/testnet/README.md) and the proposed fee floors and prover-gas table (docs/analysis/base-fee-floor.md, spec 05 section 5.10) are adopted as proposed. The three documents now say "adopted 5 October 2026" with the sign-off noted and the per-network rule written in: the testnet and the mainnet carry calibrated v1 from genesis; the devnet and the simnet keep the prototype set until the fees_v1_activation_daa height switch (fork branch release-0.3.6) moves them. Conflicts (generated site files): index.html keeps the 0.3.5 dev-fee sentence and testnet-prep's testnet-terms card; litepaper.html keeps the 0.3.5 two-paragraph dev-fee text and testnet-prep's MetaMask paragraph; journey.json keeps the 0.3.5 feed (newest 40); sitemap.xml keeps /miners and /wallet. Site rebuilt with node site/build.mjs. Also: infra/fast-time/override-60x.json carries fees_v1_activation_daa 0 (the fork's fast-time test wants every override field); docs/plans/release-0.3.6.md (the inputs push must run from this tree before the workflow verifies the signature; the devnet rollout of the fee floor as a height switch; the morning order). Checks: node site/build.mjs; link-check 324 links 0 broken; check-workflow-shell 0 findings; test-inputs-signing.sh 16 of 16 with the signer built from this tree; bash -n on the four shell files; the PowerShell parse rule at the 0.3.5 baseline (3 hits, unchanged). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
4aaff887bb
27 changed files with 1627 additions and 81 deletions
42
.github/workflows/windows.yml
vendored
42
.github/workflows/windows.yml
vendored
|
|
@ -13,7 +13,13 @@
|
|||
# Inputs that are not in git (igneumd.exe, igneum-miner.exe from the node fork; the prebuilt GPU workers with NVIDIA's
|
||||
# NVRTC DLLs) come from payload-inputs.zip on the downloads host, published by packaging/windows/push-inputs.sh on the
|
||||
# Mac; the DL_TOKEN repository secret is the path token (gh secret set DL_TOKEN < ~/.config/igneum/dl-token).
|
||||
# The Mac side of the loop is packaging/windows/fetch-ci-artifacts.sh (gh run download into the downloads folder).
|
||||
# The zip is trusted only through payload-inputs.json and its detached Ed25519 signature, made on the Mac with the
|
||||
# OTA key: the step "payload inputs" verifies the signature with the public key compiled into the app
|
||||
# (igneum-ota-sign verify-inputs embedded, built by the engine step), checks the zip's sha256 and every unpacked
|
||||
# file against the manifest, and checks the manifest's node commit against packaging/windows/node-source.pin in
|
||||
# this checkout, all before anything is built from them (review round 4, R4.5.2, ledger G13). The verified
|
||||
# manifest, its signature and the runner's record go up as the igneum-windows-inputs artifact, which
|
||||
# packaging/windows/fetch-ci-artifacts.sh re-verifies on the Mac before it will sign an update manifest.
|
||||
name: windows-ci
|
||||
on:
|
||||
push:
|
||||
|
|
@ -110,7 +116,7 @@ jobs:
|
|||
cargo build --release --locked
|
||||
ls -la target/release/igneum-app.exe
|
||||
|
||||
- name: payload inputs (payload-inputs.zip from the downloads host, sha256 checked)
|
||||
- name: payload inputs (payload-inputs.zip from the downloads host, signature, hashes and node commit verified)
|
||||
shell: bash
|
||||
env:
|
||||
DL_TOKEN: ${{ secrets.DL_TOKEN }}
|
||||
|
|
@ -121,17 +127,33 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
base="https://dl.igneum.network/dl/$DL_TOKEN"
|
||||
signer="app/igneum-app/target/release/igneum-ota-sign.exe"
|
||||
[ -x "$signer" ] || { echo "::error::$signer was not built by the engine step"; exit 1; }
|
||||
pin="packaging/windows/node-source.pin"
|
||||
[ -s "$pin" ] || { echo "::error::$pin is missing: push-inputs.sh writes it, commit it with the inputs push"; exit 1; }
|
||||
mkdir -p build/inputs "$HOME/.config/igneum"
|
||||
printf '%s' "$DL_TOKEN" > "$HOME/.config/igneum/dl-token" # make-payload.sh reads it for the update manifest URL
|
||||
curl -fsSL --retry 3 -o build/payload-inputs.json "$base/payload-inputs.json"
|
||||
curl -fsSL --retry 3 -o build/payload-inputs.sha256 "$base/payload-inputs.sha256"
|
||||
curl -fsSL --retry 3 -o build/payload-inputs.json.sig "$base/payload-inputs.json.sig"
|
||||
curl -fsSL --retry 3 -o build/payload-inputs.zip "$base/payload-inputs.zip"
|
||||
echo "$(tr -d '[:space:]' < build/payload-inputs.sha256) build/payload-inputs.zip" | sha256sum -c -
|
||||
echo "inputs manifest:"; cat build/payload-inputs.json
|
||||
# 1. the signature (the key compiled into the app), the zip's sha256 and size, the pinned node commit: all before unpacking
|
||||
"$signer" verify-inputs embedded build/payload-inputs.json build/payload-inputs.json.sig --zip build/payload-inputs.zip --node-commit "$pin"
|
||||
7z x -y -bso0 -bsp0 -obuild/inputs-unpacked build/payload-inputs.zip
|
||||
mv build/inputs-unpacked/payload-inputs/* build/inputs/
|
||||
echo "inputs manifest:"; cat build/payload-inputs.json
|
||||
# 2. every unpacked file by sha256 and size, and nothing in the folder the manifest does not name
|
||||
"$signer" verify-inputs embedded build/payload-inputs.json build/payload-inputs.json.sig --dir build/inputs
|
||||
echo "inputs:"; ls -la build/inputs
|
||||
for f in igneumd.exe igneum-miner.exe; do [ -f "build/inputs/$f" ] || { echo "::error::payload-inputs.zip has no $f"; exit 1; }; done
|
||||
# 3. the runner's record for fetch-ci-artifacts.sh, which re-verifies the signature and the pin on the Mac
|
||||
fp="$("$signer" embedded | sed -n 2p)"
|
||||
node_commit="$(jq -r .node_source_commit build/payload-inputs.json)"
|
||||
zip_sha="$(jq -r .zip.sha256 build/payload-inputs.json)"
|
||||
mkdir -p build/inputs-artifact
|
||||
cp build/payload-inputs.json build/payload-inputs.json.sig build/inputs-artifact/
|
||||
printf '{ "run_id": "%s", "run_attempt": "%s", "head_sha": "%s", "key_fingerprint": "%s", "node_commit": "%s", "zip_sha256": "%s", "verified_at": "%s" }\n' \
|
||||
"$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$GITHUB_SHA" "$fp" "$node_commit" "$zip_sha" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > build/inputs-artifact/inputs-verified.json
|
||||
cat build/inputs-artifact/inputs-verified.json
|
||||
|
||||
- name: window host (app\windows\BUILD-APP.bat, exactly as on the PC)
|
||||
shell: cmd
|
||||
|
|
@ -211,7 +233,9 @@ jobs:
|
|||
printf '| %s | %s |\n' "$(basename "$f")" "$(stat -c %s "$f")"
|
||||
done
|
||||
echo
|
||||
echo "inputs: $(tr -d '\n' < build/payload-inputs.json | head -c 400)"
|
||||
echo "inputs (signature, hashes and node commit verified): $(tr -d '\n' < build/payload-inputs.json | head -c 400)"
|
||||
echo
|
||||
echo "verified: $(cat build/inputs-artifact/inputs-verified.json)"
|
||||
} | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
|
@ -232,3 +256,9 @@ jobs:
|
|||
path: app/windows/dist/Igneum Miner.exe
|
||||
retention-days: 90
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: igneum-windows-inputs
|
||||
path: build/inputs-artifact/
|
||||
retention-days: 90
|
||||
if-no-files-found: error
|
||||
|
|
|
|||
|
|
@ -9,11 +9,18 @@
|
|||
//! igneum-ota-sign sha256 <file> the file's sha256 and size, for the manifest
|
||||
//! igneum-ota-sign sign-jobs <private-key-file> <igneum-jobs.json> the remote-jobs file (src/jobs.rs), same key
|
||||
//! igneum-ota-sign verify-jobs <public-key-file|hex> <igneum-jobs.json> <sig-file>
|
||||
//! igneum-ota-sign sign-inputs <private-key-file> <payload-inputs.json> the Windows build inputs (src/inputs.rs), same key
|
||||
//! igneum-ota-sign verify-inputs <public-key-file|hex|embedded> <payload-inputs.json> <sig-file>
|
||||
//! [--zip <payload-inputs.zip>] [--dir <unpacked folder>] [--node-commit <40 hex>]
|
||||
//! exit 0 only when the signature, the zip, every
|
||||
//! unpacked file and the pinned commit all check
|
||||
|
||||
#[path = "../manifest.rs"]
|
||||
mod manifest;
|
||||
#[path = "../jobs.rs"]
|
||||
mod jobs;
|
||||
#[path = "../inputs.rs"]
|
||||
mod inputs;
|
||||
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use std::path::Path;
|
||||
|
|
@ -115,8 +122,54 @@ fn main() {
|
|||
Err(e) => die(&e),
|
||||
}
|
||||
}
|
||||
Some("sign-inputs") if args.len() == 3 => {
|
||||
let seed = manifest::hex_decode(&read_key_arg(&args[1])).unwrap_or_else(|| die("private key is not hex"));
|
||||
let seed: [u8; 32] = seed.try_into().unwrap_or_else(|_| die("private key is not 32 bytes"));
|
||||
let sk = SigningKey::from_bytes(&seed);
|
||||
let bytes = std::fs::read(&args[2]).unwrap_or_else(|e| die(&format!("{}: {e}", args[2])));
|
||||
let text = std::str::from_utf8(&bytes).unwrap_or_else(|_| die("inputs manifest is not UTF-8"));
|
||||
let m = inputs::parse(text).unwrap_or_else(|e| die(&format!("refusing to sign: {e}")));
|
||||
eprintln!(
|
||||
"signing inputs built {} from node commit {} ({}): zip {} bytes, {} file(s)",
|
||||
m.built_at,
|
||||
&m.node_source_commit[..12],
|
||||
m.node_source_branch,
|
||||
m.zip.bytes,
|
||||
m.files.len()
|
||||
);
|
||||
println!("{}", manifest::hex_encode(&sk.sign(&bytes).to_bytes()));
|
||||
}
|
||||
Some("verify-inputs") if args.len() >= 4 => {
|
||||
let pk = if args[1] == "embedded" { manifest::OTA_PUBLIC_KEY_HEX.to_string() } else { read_key_arg(&args[1]) };
|
||||
let bytes = std::fs::read(&args[2]).unwrap_or_else(|e| die(&format!("{}: {e}", args[2])));
|
||||
let sig_text = std::fs::read_to_string(&args[3]).unwrap_or_else(|e| die(&format!("{}: {e}", args[3])));
|
||||
let sig = inputs::read_signature(&sig_text).unwrap_or_else(|e| die(&e));
|
||||
let m = inputs::verify_and_parse(&bytes, &sig, &pk).unwrap_or_else(|e| die(&format!("inputs signature: {e}")));
|
||||
let mut i = 4;
|
||||
let mut checked: Vec<String> = vec![format!("signature by {}", manifest::fingerprint(&pk))];
|
||||
while i < args.len() {
|
||||
match (args[i].as_str(), args.get(i + 1)) {
|
||||
("--zip", Some(z)) => {
|
||||
inputs::check_zip(&m, Path::new(z)).unwrap_or_else(|e| die(&e));
|
||||
checked.push(format!("zip {} ({} bytes)", m.zip.sha256, m.zip.bytes));
|
||||
}
|
||||
("--dir", Some(d)) => {
|
||||
inputs::check_dir(&m, Path::new(d)).unwrap_or_else(|e| die(&e));
|
||||
checked.push(format!("{} unpacked file(s)", m.files.len()));
|
||||
}
|
||||
("--node-commit", Some(c)) => {
|
||||
let c = if Path::new(c).is_file() { std::fs::read_to_string(c).unwrap_or_default() } else { c.to_string() };
|
||||
inputs::check_node_commit(&m, &c).unwrap_or_else(|e| die(&e));
|
||||
checked.push(format!("node commit {}", m.node_source_commit));
|
||||
}
|
||||
(flag, _) => die(&format!("unknown or incomplete argument {flag}")),
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
println!("ok: inputs built {} from node commit {} ({}); checked: {}", m.built_at, m.node_source_commit, m.node_source_branch, checked.join(", "));
|
||||
}
|
||||
_ => {
|
||||
eprintln!("usage: igneum-ota-sign keygen <priv> <pub> | sign <priv> <manifest.json> | verify <pub> <manifest.json> <sig> | embedded | fingerprint <pub> | sha256 <file> | sign-jobs <priv> <jobs.json> | verify-jobs <pub> <jobs.json> <sig>");
|
||||
eprintln!("usage: igneum-ota-sign keygen <priv> <pub> | sign <priv> <manifest.json> | verify <pub> <manifest.json> <sig> | embedded | fingerprint <pub> | sha256 <file> | sign-jobs <priv> <jobs.json> | verify-jobs <pub> <jobs.json> <sig> | sign-inputs <priv> <payload-inputs.json> | verify-inputs <pub|embedded> <payload-inputs.json> <sig> [--zip z] [--dir d] [--node-commit c]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
281
app/igneum-app/src/inputs.rs
Normal file
281
app/igneum-app/src/inputs.rs
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
//! The signed payload-inputs manifest (review round 4, R4.5.2, ledger G13).
|
||||
//!
|
||||
//! The Windows build on GitHub's runner cannot make the node, the miner or the GPU workers (they come from the
|
||||
//! node fork, which is not in the repository, and from NVIDIA's redistributables). Those files travel as
|
||||
//! `payload-inputs.zip` on the downloads host. Before 4 October 2026 the runner checked the zip against a sha256
|
||||
//! served beside it, which is a transfer check, not an authentication: whoever controls the host controls the
|
||||
//! binaries, and the Mac then signed the update manifest over whatever the run produced.
|
||||
//!
|
||||
//! Now `packaging/windows/push-inputs.sh` writes `payload-inputs.json` (this format), signs it on the Mac with the
|
||||
//! OTA key (`igneum-ota-sign sign-inputs`) and uploads the signature beside it. The workflow verifies the signature
|
||||
//! with the public key compiled into the app (`manifest::OTA_PUBLIC_KEY_HEX`) before it builds anything, checks
|
||||
//! the zip's sha256 and every unpacked file against the manifest, and checks the pinned node source commit
|
||||
//! against `packaging/windows/node-source.pin` in the commit it builds. `fetch-ci-artifacts.sh` refuses to sign an
|
||||
//! update manifest unless the run's verified inputs manifest re-verifies on the Mac.
|
||||
//!
|
||||
//! The bytes signed are the file as uploaded. `parse` refuses anything it does not understand, so a manifest the
|
||||
//! signer would not sign is also one the verifier would not accept.
|
||||
|
||||
use crate::manifest::{hex_decode, sha256_file, verify_signature};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// The format tag every manifest must carry.
|
||||
pub const FORMAT: &str = "igneum-payload-inputs/1";
|
||||
|
||||
/// Files the payload cannot do without; the verifier refuses a manifest that omits one.
|
||||
pub const REQUIRED_FILES: &[&str] = &["igneumd.exe", "igneum-miner.exe"];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FileEntry {
|
||||
pub sha256: String,
|
||||
pub bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct InputsManifest {
|
||||
pub format: String,
|
||||
/// When the zip was built, UTC, `YYYY-MM-DDTHH:MM:SSZ`.
|
||||
pub built_at: String,
|
||||
/// The node fork commit the exes were built from (40 hex), and its branch (informational).
|
||||
pub node_source_commit: String,
|
||||
pub node_source_branch: String,
|
||||
/// The main repository commit `push-inputs.sh` ran at (40 hex; informational).
|
||||
pub repo_commit: String,
|
||||
/// The zip as uploaded.
|
||||
pub zip: FileEntry,
|
||||
/// Every file inside the zip's `payload-inputs/` folder, by name.
|
||||
pub files: BTreeMap<String, FileEntry>,
|
||||
}
|
||||
|
||||
fn is_hex(s: &str, len: usize) -> bool {
|
||||
s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit()) && s.bytes().all(|b| !b.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
fn check_entry(name: &str, e: &FileEntry) -> Result<(), String> {
|
||||
if !is_hex(&e.sha256, 64) {
|
||||
return Err(format!("{name}: sha256 is not 64 lowercase hex characters"));
|
||||
}
|
||||
if e.bytes == 0 {
|
||||
return Err(format!("{name}: bytes is 0"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses and validates a manifest. Unknown fields, missing fields, a wrong format tag, a malformed hash or
|
||||
/// commit, an empty file list or a missing required file are all refused.
|
||||
pub fn parse(text: &str) -> Result<InputsManifest, String> {
|
||||
let m: InputsManifest = serde_json::from_str(text).map_err(|e| format!("inputs manifest: {e}"))?;
|
||||
if m.format != FORMAT {
|
||||
return Err(format!("inputs manifest: format is {:?}, this build understands {FORMAT:?}", m.format));
|
||||
}
|
||||
if m.built_at.len() != 20 || !m.built_at.ends_with('Z') || m.built_at.as_bytes()[10] != b'T' {
|
||||
return Err("inputs manifest: built_at is not YYYY-MM-DDTHH:MM:SSZ".into());
|
||||
}
|
||||
if !is_hex(&m.node_source_commit, 40) {
|
||||
return Err("inputs manifest: node_source_commit is not a 40-character lowercase hex commit".into());
|
||||
}
|
||||
if !is_hex(&m.repo_commit, 40) {
|
||||
return Err("inputs manifest: repo_commit is not a 40-character lowercase hex commit".into());
|
||||
}
|
||||
if m.node_source_branch.trim().is_empty() {
|
||||
return Err("inputs manifest: node_source_branch is empty".into());
|
||||
}
|
||||
check_entry("zip", &m.zip)?;
|
||||
if m.files.is_empty() {
|
||||
return Err("inputs manifest: files is empty".into());
|
||||
}
|
||||
for (name, e) in &m.files {
|
||||
if name.is_empty() || name.contains('/') || name.contains('\\') || name == "." || name == ".." {
|
||||
return Err(format!("inputs manifest: {name:?} is not a plain file name"));
|
||||
}
|
||||
check_entry(name, e)?;
|
||||
}
|
||||
for r in REQUIRED_FILES {
|
||||
if !m.files.contains_key(*r) {
|
||||
return Err(format!("inputs manifest: no {r} in files"));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// Verifies the detached signature over the exact bytes, then parses.
|
||||
pub fn verify_and_parse(bytes: &[u8], sig_hex: &str, pub_hex: &str) -> Result<InputsManifest, String> {
|
||||
verify_signature(bytes, sig_hex, pub_hex)?;
|
||||
let text = std::str::from_utf8(bytes).map_err(|_| "inputs manifest is not UTF-8")?;
|
||||
parse(text)
|
||||
}
|
||||
|
||||
/// The zip on disk must be the one the manifest names: same sha256, same size.
|
||||
pub fn check_zip(m: &InputsManifest, zip: &Path) -> Result<(), String> {
|
||||
let sum = sha256_file(zip).map_err(|e| format!("{}: {e}", zip.display()))?;
|
||||
let size = std::fs::metadata(zip).map(|md| md.len()).unwrap_or(0);
|
||||
if sum != m.zip.sha256 {
|
||||
return Err(format!("{}: sha256 {sum} is not the manifest's {}", zip.display(), m.zip.sha256));
|
||||
}
|
||||
if size != m.zip.bytes {
|
||||
return Err(format!("{}: {size} bytes, the manifest says {}", zip.display(), m.zip.bytes));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The unpacked folder must hold exactly the manifest's files, each with its sha256 and size. A file the manifest
|
||||
/// does not name is refused too: nothing rides into the payload unsigned.
|
||||
pub fn check_dir(m: &InputsManifest, dir: &Path) -> Result<(), String> {
|
||||
let mut seen = 0usize;
|
||||
let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name == ".DS_Store" {
|
||||
continue;
|
||||
}
|
||||
let Some(want) = m.files.get(&name) else {
|
||||
return Err(format!("{name}: in the folder but not in the signed manifest"));
|
||||
};
|
||||
let p = entry.path();
|
||||
let sum = sha256_file(&p).map_err(|e| format!("{name}: {e}"))?;
|
||||
let size = std::fs::metadata(&p).map(|md| md.len()).unwrap_or(0);
|
||||
if sum != want.sha256 || size != want.bytes {
|
||||
return Err(format!("{name}: sha256 {sum} ({size} bytes) is not the manifest's {} ({} bytes)", want.sha256, want.bytes));
|
||||
}
|
||||
seen += 1;
|
||||
}
|
||||
if seen != m.files.len() {
|
||||
let missing: Vec<&String> = m.files.keys().filter(|k| !dir.join(k).is_file()).collect();
|
||||
return Err(format!("the folder holds {seen} of the manifest's {} files; missing {:?}", m.files.len(), missing));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The commit the manifest pins must be the commit the repository expects (`packaging/windows/node-source.pin`).
|
||||
pub fn check_node_commit(m: &InputsManifest, expected: &str) -> Result<(), String> {
|
||||
let expected = expected.trim();
|
||||
if !is_hex(expected, 40) {
|
||||
return Err(format!("expected node commit {expected:?} is not a 40-character lowercase hex commit"));
|
||||
}
|
||||
if m.node_source_commit != expected {
|
||||
return Err(format!("the manifest pins node commit {} but the repository expects {expected}", m.node_source_commit));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A signature file holds 128 hex characters and nothing else of substance.
|
||||
pub fn read_signature(text: &str) -> Result<String, String> {
|
||||
let s = text.trim();
|
||||
match hex_decode(s) {
|
||||
Some(b) if b.len() == 64 => Ok(s.to_string()),
|
||||
_ => Err("signature is not 128 hex characters".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::manifest::hex_encode;
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const SHA: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
const COMMIT: &str = "6aa69a45364b9b30a32695e33eb66f100c9be85f";
|
||||
|
||||
fn sample() -> String {
|
||||
format!(
|
||||
r#"{{"format":"{FORMAT}","built_at":"2026-10-04T20:07:21Z","node_source_commit":"{COMMIT}","node_source_branch":"finality-fixes","repo_commit":"{COMMIT}","zip":{{"sha256":"{SHA}","bytes":123}},"files":{{"igneumd.exe":{{"sha256":"{SHA}","bytes":1}},"igneum-miner.exe":{{"sha256":"{SHA}","bytes":2}}}}}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn key() -> (SigningKey, String) {
|
||||
let sk = SigningKey::from_bytes(&[7u8; 32]);
|
||||
let pk = hex_encode(sk.verifying_key().as_bytes());
|
||||
(sk, pk)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_a_good_manifest() {
|
||||
let m = parse(&sample()).unwrap();
|
||||
assert_eq!(m.node_source_commit, COMMIT);
|
||||
assert_eq!(m.files.len(), 2);
|
||||
assert_eq!(m.zip.bytes, 123);
|
||||
check_node_commit(&m, COMMIT).unwrap();
|
||||
assert!(check_node_commit(&m, &COMMIT.replace('6', "7")).unwrap_err().contains("expects"));
|
||||
assert!(check_node_commit(&m, "6aa69a45").unwrap_err().contains("40-character"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_what_the_signer_would_not_sign() {
|
||||
let good = sample();
|
||||
let cases = [
|
||||
(good.replace(FORMAT, "igneum-payload-inputs/2"), "format"),
|
||||
(good.replace("\"node_source_branch\":\"finality-fixes\",", ""), "missing field"),
|
||||
(good.replace("\"zip\":", "\"extra\":1,\"zip\":"), "unknown field"),
|
||||
(good.replace(&format!("\"node_source_commit\":\"{COMMIT}\""), "\"node_source_commit\":\"6aa69a45\""), "node_source_commit"),
|
||||
(good.replace("2026-10-04T20:07:21Z", "2026-10-04 20:07:21"), "built_at"),
|
||||
(good.replace("\"bytes\":123", "\"bytes\":0"), "bytes is 0"),
|
||||
(good.replace("\"igneum-miner.exe\"", "\"igneum-miner.exe.bak\""), "no igneum-miner.exe"),
|
||||
(good.replace("\"igneumd.exe\"", "\"../igneumd.exe\""), "plain file name"),
|
||||
(good.replace(SHA, &SHA.to_uppercase()), "lowercase hex"),
|
||||
];
|
||||
for (text, why) in cases {
|
||||
let err = parse(&text).unwrap_err();
|
||||
assert!(err.contains(why), "{why}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_verify_and_tamper() {
|
||||
let (sk, pk) = key();
|
||||
let bytes = sample().into_bytes();
|
||||
let sig = hex_encode(&sk.sign(&bytes).to_bytes());
|
||||
assert_eq!(read_signature(&format!("{sig}\n")).unwrap(), sig);
|
||||
assert!(read_signature("abc").is_err());
|
||||
let m = verify_and_parse(&bytes, &sig, &pk).unwrap();
|
||||
assert_eq!(m.node_source_commit, COMMIT);
|
||||
// one byte changed anywhere: the signature no longer verifies
|
||||
let mut tampered = bytes.clone();
|
||||
let i = tampered.iter().position(|b| *b == b'1').unwrap();
|
||||
tampered[i] = b'2';
|
||||
assert!(verify_and_parse(&tampered, &sig, &pk).is_err());
|
||||
// a different key: refused
|
||||
let other = hex_encode(SigningKey::from_bytes(&[9u8; 32]).verifying_key().as_bytes());
|
||||
assert!(verify_and_parse(&bytes, &sig, &other).is_err());
|
||||
// the embedded OTA key refuses a signature from this test key
|
||||
assert!(verify_and_parse(&bytes, &sig, crate::manifest::OTA_PUBLIC_KEY_HEX).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_and_folder_checks() {
|
||||
let dir = std::env::temp_dir().join(format!("igneum-inputs-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join("unpacked")).unwrap();
|
||||
std::fs::write(dir.join("unpacked/igneumd.exe"), b"node").unwrap();
|
||||
std::fs::write(dir.join("unpacked/igneum-miner.exe"), b"miner!").unwrap();
|
||||
std::fs::write(dir.join("payload-inputs.zip"), b"zipzipzip").unwrap();
|
||||
let sha = |p: &Path| sha256_file(p).unwrap();
|
||||
let text = format!(
|
||||
r#"{{"format":"{FORMAT}","built_at":"2026-10-04T20:07:21Z","node_source_commit":"{COMMIT}","node_source_branch":"finality-fixes","repo_commit":"{COMMIT}","zip":{{"sha256":"{}","bytes":9}},"files":{{"igneumd.exe":{{"sha256":"{}","bytes":4}},"igneum-miner.exe":{{"sha256":"{}","bytes":6}}}}}}"#,
|
||||
sha(&dir.join("payload-inputs.zip")),
|
||||
sha(&dir.join("unpacked/igneumd.exe")),
|
||||
sha(&dir.join("unpacked/igneum-miner.exe"))
|
||||
);
|
||||
let m = parse(&text).unwrap();
|
||||
check_zip(&m, &dir.join("payload-inputs.zip")).unwrap();
|
||||
check_dir(&m, &dir.join("unpacked")).unwrap();
|
||||
// a changed byte in the zip
|
||||
std::fs::write(dir.join("payload-inputs.zip"), b"zipzipzip!").unwrap();
|
||||
assert!(check_zip(&m, &dir.join("payload-inputs.zip")).unwrap_err().contains("sha256"));
|
||||
// an unlisted file in the folder
|
||||
std::fs::write(dir.join("unpacked/extra.dll"), b"x").unwrap();
|
||||
assert!(check_dir(&m, &dir.join("unpacked")).unwrap_err().contains("not in the signed manifest"));
|
||||
std::fs::remove_file(dir.join("unpacked/extra.dll")).unwrap();
|
||||
// a changed file
|
||||
std::fs::write(dir.join("unpacked/igneumd.exe"), b"nodE").unwrap();
|
||||
assert!(check_dir(&m, &dir.join("unpacked")).unwrap_err().contains("igneumd.exe"));
|
||||
// a missing file
|
||||
std::fs::remove_file(dir.join("unpacked/igneumd.exe")).unwrap();
|
||||
assert!(check_dir(&m, &dir.join("unpacked")).unwrap_err().contains("missing"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
139
docs/analysis/base-fee-floor.md
Normal file
139
docs/analysis/base-fee-floor.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Base-fee floors and the prover-gas table: the model (ADOPTED 5 October 2026)
|
||||
|
||||
Status: every number below was proposed on the night of 4 October 2026 and ADOPTED by the owner on 5 October 2026,
|
||||
as proposed (sign-off recorded in `docs/plans/release-0.3.6.md`). The parameters live in the node fork
|
||||
(`consensus/core/src/fees.rs`, `FeeParams::CALIBRATED_V1`; written on branch `testnet-params`, merged into
|
||||
`release-0.3.6` on 5 October 2026). Which network runs them: the testnet and the mainnet from genesis
|
||||
(`Params.fees` = v1, `fees_v1_activation_daa` = 0); the devnet and the simnet keep `FeeParams::PROTOTYPE` until the
|
||||
override file carries a `fees` object or the `fees_v1_activation_daa` height switch (section 4, "the devnet
|
||||
rollout"), so the live devnet does not change rules between the 0.3.5 and 0.3.6 node builds. Spec 05 section 5.10
|
||||
carries the summary. Nothing is deployed.
|
||||
|
||||
Inputs the model takes from the repository, with their status:
|
||||
|
||||
| Input | Value | Status, source |
|
||||
|---|---|---|
|
||||
| Block rate | 1 block per second | Designed (spec 02; `BlockrateParams::new::<1>()`) |
|
||||
| Execution gas per block `B_e` | 30,000,000 | Implemented, devnet v3 value (`consensus/core/src/evm.rs`, `BLOCK_EXECUTION_GAS_LIMIT`) |
|
||||
| Plain transfer, execution gas | 21,000 | Ethereum's rule |
|
||||
| Year-one block subsidy | 31.69 IGN (3,168,808,781 sompi per second) | Implemented (`consensus/core/src/igneum.rs`) |
|
||||
| SP1 cycles per EVM gas, modexp-heavy shard | 44 | Measured (bench-log, "shard proving on the RTX 5090", run-20261004-173115) |
|
||||
| SP1 cycles per prototype pgas, same shard | 9 | Measured (same) |
|
||||
| SP1 cycles per prototype pgas, plain-transfer shard | 1,400 to 1,600 | Measured (bench-log, 4 October, "proving: devnet v4 shards") |
|
||||
| Compressed proof of a 60 M-cycle shard, one RTX 5090 | 10.9 s | Measured (same run) |
|
||||
| Aggregation of a block's shards | 2.2 to 2.5 s | Measured (same run) |
|
||||
| Token price | $0.10 per IGN | ASSUMPTION for the arithmetic only; sensitivities at $0.01, $1 and $2 below. Not a forecast, not a claim |
|
||||
| Electricity | $0.15 per kWh; RTX 5090 at 575 W while proving | Approximate (the economy analysis used $0.02 to $0.40; 575 W is the card's rated draw, not measured here) |
|
||||
|
||||
## 1. The prover-gas table, calibrated v1
|
||||
|
||||
The unit is unchanged: 1 pgas stands for 1,000 reference SP1 cycles. The prototype table of 3 October charged every
|
||||
opcode and precompile by shape with magnitudes nobody had measured. Two of its entries are now measured.
|
||||
|
||||
| Entry | Prototype (3 October) | Measurement | Calibrated v1 |
|
||||
|---|---|---|---|
|
||||
| modexp (0x05) | 1,000 + 10 per input byte | the modexp-dominated shard ran 60.76 M cycles for 6.75 M prototype pgas: 9 cycles per pgas against the unit's 1,000, so the entry is about 111x its cost | 10 + 1 per 10 input bytes (the prototype over 100) |
|
||||
| Intrinsic per transaction | 200 | the plain-transfer shard ran 1,400 to 1,600 cycles per prototype pgas: 200 x 1,500 = 300,000 cycles per transaction, which includes the shard's fixed witness check and root computations, so it is an upper bound | 300 |
|
||||
| Every other opcode and precompile | prototype shape | not measured | prototype shape, unchanged, table version 1 |
|
||||
|
||||
What the two constants say about a transaction: the modexp shard metered 1,390,773 EVM gas for 60.76 M cycles, 44
|
||||
cycles per gas, which is 0.044 pgas per gas at the unit; a plain transfer is 300 pgas for 21,000 gas, 0.014 pgas per
|
||||
gas. The design expected a `pgas / gas` band of 0.1 to 10 (execution-layer design 4.3); the measured band is 0.01 to
|
||||
0.05, so proving gas is cheaper per gas than the design guessed, by 10x, on the two workloads measured. The
|
||||
remaining entries (ecrecover 3,000 pgas, ecpairing 45,000 per pair, the storage opcodes) are the next calibration;
|
||||
each is one SP1 run of a fixture that isolates it.
|
||||
|
||||
## 2. The shard and block budgets
|
||||
|
||||
| Quantity | Value | Arithmetic |
|
||||
|---|---|---|
|
||||
| Shard budget `S_p` | 30,000 pgas | 30 M cycles: half the measured 60 M-cycle shard. One RTX 5090 compresses it in about 5.5 s (linear in cycles from 10.9 s, approximate); a 12 GB card in about 20 s (approximate: the economy simulation's shard shares put a 3060 at 3.7x the 5090's time; unmeasured, the phase 2 gate) |
|
||||
| Block budget `B_p` | 120,000 pgas | 4 x `S_p`, the prototype's ratio (spec 7.4) |
|
||||
| Transfers per block at `B_p` | 400 | 120,000 / 300 |
|
||||
| Execution gas those use | 8,400,000 | 400 x 21,000, 28% of `B_e`: the proving dimension binds first for transfers |
|
||||
| Block proof time, four RTX 5090s | about 8 s | 5.5 s per shard in parallel plus 2.5 s aggregation (approximate), inside the 20 to 60 s launch target |
|
||||
| Block proof time, four 12 GB cards | about 23 s | 20 + 2.5 s (approximate) |
|
||||
| Cards to keep pace at full blocks | 22 RTX 5090s, or about 80 12 GB cards | 4 shards x 5.5 s = 22 card-seconds per second; x 3.7 for the 12 GB class (approximate) |
|
||||
|
||||
The prototype `B_p` of 30,000,000 pgas was "equal to `B_e`" and never a throughput number: at 9 cycles per prototype
|
||||
pgas a full prototype block is 270 M cycles, 49 s on one 5090, and at the plain-transfer rate it is 45 G cycles. The
|
||||
calibrated `B_p` is a throughput number: one block per second provable by a fleet the economy simulation already
|
||||
models. Raising it is a parameter the genesis rules leave to miners (60% signalling, spec 5.5), and the economy
|
||||
analysis of 4 October recommends tying it to the live proving fleet on the testnet.
|
||||
|
||||
## 3. The base-fee floors
|
||||
|
||||
Both base fees are burned in full (spec 5.1) and adjusted by EIP-1559 toward half the limit with a denominator of 8
|
||||
(1/8 per block at the extremes). The floor is the lowest value either fee can reach. It has three jobs: keep a plain
|
||||
transfer cheap, make a full block cost real money from the first block, and price proving above the electricity it
|
||||
burns so spam cannot be cheaper than the work it imposes.
|
||||
|
||||
| Floor | Value | In IGN |
|
||||
|---|---|---|
|
||||
| Execution base fee `f_e` | 100 gwei per gas | 0.0000001 IGN per gas |
|
||||
| Proving base fee `f_p` | 10,000 gwei per pgas | 0.00001 IGN per pgas |
|
||||
| Initial base fees at genesis | the floors | |
|
||||
|
||||
### A plain transfer at the floor
|
||||
|
||||
| Term | Arithmetic | IGN |
|
||||
|---|---|---|
|
||||
| Execution | 21,000 x 100 gwei | 0.0021 |
|
||||
| Proving | 300 x 10,000 gwei | 0.0030 |
|
||||
| Total (tip excluded) | | 0.0051 |
|
||||
|
||||
| Token price (assumption) | $0.01 | $0.10 | $1 | $2 |
|
||||
|---|---|---|---|---|
|
||||
| Transfer at the floor | $0.000051 | $0.00051 | $0.0051 | $0.0102 |
|
||||
|
||||
The target "under $0.01 per simple transfer" holds up to $1.96 per IGN. Under load the fee leaves the floor: after
|
||||
`n` consecutive full blocks the base fee is the floor times 1.125^n, which is 3.2x after 10 blocks, 34x after 30 and
|
||||
about 1,170x after 60 blocks (one minute). The floor prices the quiet chain; the controller prices the busy one.
|
||||
|
||||
### A full block at the floor, which is what spam costs
|
||||
|
||||
| Case | Arithmetic | IGN per block | Per day (86,400 blocks) | At $0.10 per day |
|
||||
|---|---|---|---|---|
|
||||
| Execution dimension full (30 M gas of cheap-to-prove calls) | 30,000,000 x 100 gwei | 3.0 | 259,200 | $25,920 |
|
||||
| Proving dimension full with transfers (400 transfers) | 120,000 x 10,000 gwei + 8,400,000 x 100 gwei | 1.2 + 0.84 = 2.04 | 176,256 | $17,626 |
|
||||
| Both dimensions full (the worst mix) | | up to 4.2 | 362,880 | $36,288 |
|
||||
|
||||
A self-paying spam loop (a miner filling its own blocks, or a contract that calls itself until the gas is gone) pays
|
||||
the same: the base fee is burned, the tip returns to the miner and nets to zero, so a miner that fills its own block
|
||||
burns 3.0 IGN against a subsidy of 31.69 IGN, 9.5% of its own reward per filled block, for nothing. And only at the
|
||||
floor: after one minute of full blocks the controller has multiplied every number above by about 1,170.
|
||||
|
||||
### Proving priced above its electricity
|
||||
|
||||
| Quantity | Arithmetic | Value |
|
||||
|---|---|---|
|
||||
| Cycles per second, one RTX 5090 | 60 M cycles / 10.9 s | 5.5 M |
|
||||
| Energy per pgas (1,000 cycles) | 575 W x 1,000 / 5.5 M | 0.105 J = 2.9 x 10^-8 kWh |
|
||||
| Electricity per pgas at $0.15 per kWh | | $4.4 x 10^-9 |
|
||||
| Floor per pgas at $0.10 per IGN | 0.00001 IGN | $1.0 x 10^-6 |
|
||||
| Floor over electricity | | 230x at $0.10; 23x at $0.01; 1x at $0.00044 |
|
||||
|
||||
The floor covers the physical cost of the proving it buys down to a token price of about $0.0004, which is where
|
||||
this anchor would bind before the spam anchor does. The execution dimension has no such anchor: native execution of
|
||||
a full block costs tens of milliseconds of CPU; its floor is set by the spam arithmetic alone, as Ethereum's is.
|
||||
|
||||
## 4. What the table and the floors do not settle
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| The other opcode and precompile entries | prototype shapes; calibrate per entry in SP1 with three input sizes (design R1) |
|
||||
| The intrinsic 300 | an upper bound that includes the per-shard fixed cost; a shard with many transfers will show the marginal number |
|
||||
| The 12 GB card time for `S_p` | approximate, from the simulation's share ratios; the phase 2 gate measures it |
|
||||
| The prover's mirror of the table | `proving/igneum-prove/core/src/config.rs` and `pgas.rs` carry the prototype values at `b7fca5a0`; they must change in lockstep with the node (the guest program's id changes, every fixture is re-cut at the new `S_p`), or the shard statement differs from the node's. Not changed tonight |
|
||||
| The devnet rollout | done as a height switch (5 October 2026): `Params.fees_v1_activation_daa` (override file, default never on devnet and simnet, 0 on testnet and mainnet, in the consensus digest). Chain blocks at or above the switch meter with v1 (every metering site in `igneum/exec` takes the block's DAA score, `fees::fee_params_at`); the first such block raises both base fees to the v1 floors. The live devnet keeps its history and its prototype rules until the switch is published with the 0.3.6 update (`docs/plans/release-0.3.6.md`, section 5). A fresh chain can instead carry `fees` in the override file (the fast-time profile does) |
|
||||
| The price assumption | $0.10 is an arithmetic assumption. The floor is a parameter the genesis rules leave to miners (60% signalling) and can be moved by them |
|
||||
|
||||
## 5. Where the numbers live
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| The parameter set and its tests | `vendor/igneum-node-testnet/consensus/core/src/fees.rs` (branch `testnet-params`) |
|
||||
| Per network | `consensus/core/src/config/params.rs`, `Params.fees` and `Params.fees_v1_activation_daa` (`FeeParams::TESTNET` and `MAINNET` = `CALIBRATED_V1` with the switch at 0; `FeeParams::DEVNET` and `SIMNET` = `PROTOTYPE` with the switch never, both movable through the override file) |
|
||||
| The execution layer's readers | `igneum/exec/src/config.rs` (`block_proving_gas_limit(daa)`, `intrinsic_pgas_per_tx(daa)`, the floor and initial readers, every one at a DAA score), `pgas.rs` (the inspector carries the block's table; the modexp entry reads it), `executor.rs` (`execute_segment` picks the set by the block's DAA score and raises the carried base fees to its floors; `next_base_fee` takes the floor and the denominator) |
|
||||
| Installed at start | `kaspad/src/daemon.rs` (`install_fee_params`: the base set and the switch, printed after the PoW schedule) |
|
||||
| The specification | `docs/spec/05-fees-and-economics.md` section 5.10 |
|
||||
121
docs/plans/history-rewrite.md
Normal file
121
docs/plans/history-rewrite.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# G14: the history rewrite, exact plan and dry-run result (4 October 2026, night)
|
||||
|
||||
Internal. Extends `docs/fud-fixes.md` section 5 (step 4) with the exact commands, what the dry run showed, what
|
||||
breaks, and the order for the morning. Nothing here has touched the real repository: the dry run ran on a throwaway
|
||||
mirror clone under the session scratchpad and nothing was pushed. The owner is not named in this file; "the first
|
||||
name" and "the login" stand for the values the script reads from the history itself.
|
||||
|
||||
## 1. What the history holds today (counts from the real repository, 4 October 2026, 22:30 UTC)
|
||||
|
||||
| Item | Count | Where |
|
||||
|---|---|---|
|
||||
| Commits | 363 on all branches | |
|
||||
| Commits stamped `+0100` (author or committer) | 291 of 363 | the UK or Irish summer offset; 72 are `+0000` |
|
||||
| Commits authored with the personal name | 40 (31 on the old GitHub noreply address, 9 on the personal address) | the commits before the 3 October identity rule |
|
||||
| Commits as the standing login `igneum-josh` | 323 | |
|
||||
| The intake key | 6 tracked files, 8 commits (`78df757` to `4c9810f`) | `packaging/mac/packaged-config.sh`, `infra/gpu-bench/upload.sh`, `proving/windows-wsl2/prove-block.sh`, `prove-shard.sh`, `proto-cuda/windows-miner/upload-log.bat`, `proto-cuda/windows-app/upload-log.bat` |
|
||||
| The dl token | 1 tracked file, 1 commit (`c47ff03`) | `docs/plans/morning-2026-10-04.md` |
|
||||
| The `.next` rotations of both | 0 files, 0 commits | `~/.config/igneum/log-intake-key.next`, `dl-token.next` (4 October 19:25) are not in the tree |
|
||||
| The relay key and token (current and old) | 0 files, 0 commits | |
|
||||
| The review files | `docs/fud-ledger.md` (36 commits from `39c20b7`), `docs/fud-fixes.md` (6 from `e7545d5`), `docs/review/` (4 from `5ab296c`), `site/ledger.html` (5 from `0ec11be`) | tracked, not ignored |
|
||||
| Tracked files carrying the first name (case-insensitive) | 71 at HEAD; 93 commits touch such content; 10 commit messages carry it | `CLAUDE.md`, the agent file, plans, packaging, the app's WSL paths, the Chrome profile rule |
|
||||
| The surname | 4 files at HEAD | |
|
||||
| The other businesses' names, the registrar, the database id, home paths | vivanmn 6, peasehill 5, thrsty 4, godaddy 7, soft-voice 3, `/Users/` 22, quantum 4 | identity terms are rewritten by the history pass below; providers and paths are the public-export scrub's job (`tools/ci/forbidden-strings.txt`), not this pass |
|
||||
|
||||
## 2. The rewrite, exactly
|
||||
|
||||
Tool: `git-filter-repo` 2.47.0 (not installed on the Mac; the dry run used a pip install into the scratchpad,
|
||||
`python3 -m pip install --target <dir> git-filter-repo`, run as `python3 <dir>/git_filter_repo.py`). It refuses to
|
||||
run on anything but a fresh clone, which is the safety the plan relies on.
|
||||
|
||||
The script is `dryrun.sh` in the scratchpad (`rewrite/`); it reads every value from the history and from
|
||||
`~/.config/igneum` at run time and writes the replacement files with mode 0600, then deletes them. The one
|
||||
invocation, with the files it writes:
|
||||
|
||||
```
|
||||
git clone --mirror <repo> clone && cd clone
|
||||
python3 git_filter_repo.py --force \
|
||||
--invert-paths --path docs/fud-ledger.md --path docs/fud-fixes.md --path docs/review --path site/ledger.html \
|
||||
--replace-text replace.txt \
|
||||
--replace-message messages.txt \
|
||||
--mailmap mailmap \
|
||||
--commit-callback '
|
||||
for attr in ("author_date", "committer_date"):
|
||||
d = getattr(commit, attr); parts = d.split(b" ")
|
||||
if len(parts) == 2 and parts[1] != b"+0000":
|
||||
setattr(commit, attr, parts[0] + b" +0000")
|
||||
'
|
||||
```
|
||||
|
||||
| File | Lines (values never written in this plan) |
|
||||
|---|---|
|
||||
| `replace.txt` (blob text) | `literal:<intake key>==>***INTAKE-KEY-REMOVED***`; `literal:<dl token>==>***DL-TOKEN-REMOVED***`; the two personal `Name <email>` strings to the standing login string; the personal email and the old noreply address to `[removed]`; `regex:\bFirst's\b==>the project lead's`; `regex:\bFirst\s+Last\b==>the project lead`; `regex:\bFirst\b==>the project lead`; `regex:\bLast\b==>[removed]`; `regex:(?i)(?<!igneum-)\bfirst\b==>[user]` (the lowercase user-name form in Windows and WSL paths, added after dry run 1 left 20 lines); `regex:(?i)\b<second login>\b==>[second-owner-login]`; `regex:(?i)\b(vivanmn\|peasehill\|thrsty\|gemven\|jbm exec)\b==>[other-business]` |
|
||||
| `messages.txt` (commit messages) | the first-name rules and the second-login rule |
|
||||
| `mailmap` | both personal identities to `igneum-josh <337424239+igneum-josh@users.noreply.github.com>` |
|
||||
|
||||
The date callback keeps the instant and rewrites the offset to `+0000`, so no commit moves in time; only the
|
||||
`+0100` fingerprint goes. `--invert-paths` drops the four internal files from every commit, which empties the
|
||||
commits that touched nothing else; filter-repo prunes those.
|
||||
|
||||
## 3. The dry run (two passes on the mirror clone, 4 October 2026, 22:35 to 22:55 UTC)
|
||||
|
||||
| Check | Before | After pass 2 |
|
||||
|---|---|---|
|
||||
| Commits | 364 in the mirror (363 plus the in-progress branch head) | 312: the 52 commits that only touched the dropped files are gone |
|
||||
| Author and committer identities | 3 | 1: the standing login on all 312 |
|
||||
| Timezone offsets (author and committer, 624 stamps) | 291 x 2 `+0100` | 624 `+0000` |
|
||||
| `git log -S<intake key>` | 8 commits | 0 |
|
||||
| `git log -S<dl token>` | 1 commit | 0 |
|
||||
| Commits touching the four dropped files | 51 | 0 |
|
||||
| Identity grep over every blob in the history (first name outside the login, surname, second login, personal addresses, the other businesses; case-insensitive) | thousands of lines | 0 lines |
|
||||
| Identity grep over commit metadata (names, addresses, subjects, bodies) | | 0 lines |
|
||||
| `CLAUDE.md` line 4 after the pass | the full name | "the project lead's project, started 3 October 2026" |
|
||||
| Runtime | | 2 min 58 s for the filter, 3 min 15 s with the greps |
|
||||
|
||||
Pass 1 (case-sensitive name rules only) left 20 blob lines and 2 message lines: the lowercase user-name form in
|
||||
`C:\Users\<first>` and WSL paths in `app/igneum-app/src/jobrun.rs`, `prover.rs`, `docs/plans/shard-test-pc2.md`,
|
||||
`packaging/README-ship.md`, `packaging/ota/publish-jobs.sh`, `relay/playbooks/shard-test.ps1` and the Chrome-profile
|
||||
line of `CLAUDE.md`. The `(?i)(?<!igneum-)` rule closed them in pass 2.
|
||||
|
||||
What the pass does NOT do, by design, and must be done by hand or by the owner:
|
||||
|
||||
| Gap | Why | Who |
|
||||
|---|---|---|
|
||||
| The standing login `igneum-josh` carries the first name inside it, in every commit's author line and in every file that names the login | A login is a GitHub setting, not a text rule: renaming it is one setting, the numeric noreply id stays, then one more mailmap line (`<new> <337424239+<new>@...> <337424239+igneum-josh@...>`) and one more replace rule (`igneum-josh` to the new login) go into the same pass | the owner (rename), then the script |
|
||||
| `CLAUDE.md` as a public file (section 5 step 2 of `docs/fud-fixes.md`: the registrar, the database id, the browser-profile section, the tooling links) | The pass replaces names; it does not rewrite paragraphs. The scrubbed `CLAUDE.md` of step 2 replaces the file in every commit with `--path-rename` or a blob callback once it exists | Claude, after the owner approves the public text |
|
||||
| The second owner login is still an organisation owner | GitHub setting (decision e: one anonymous owner) | the owner |
|
||||
| Providers, hosts, home paths, machine names | the public-export scrub (`tools/ci/forbidden-strings.txt`, `igneum-public/tools/sync.sh`); the private repository keeps them until the public date | the export |
|
||||
|
||||
## 4. What breaks when the rewrite is applied for real
|
||||
|
||||
| What | Why | Recovery |
|
||||
|---|---|---|
|
||||
| Every worktree of the main checkout (16 today: `igneum-wt-appui`, `bughunt`, `buildjob`, `devfee`, `eff`, `finality`, `latency`, `perf`, `redteam`, `release`, `reliability`, `ship`, `site`, `wallet`, `testnet`, plus two under the scratchpad) | Their HEADs point at old commit ids that no longer exist in the rewritten history; `git status` still works on the old objects, `git pull` and `git rebase` do not | Each agent commits and pushes its branch before the freeze; after the rewrite every branch is re-created from the rewritten refs: `git worktree remove`, `git worktree add ../igneum-wt-<name> <branch>` |
|
||||
| Agents' branches (15 local, 11 on origin) | Rewritten with everything else (the mirror clone carries every ref), so the branch names survive with new ids; an agent that keeps an old local branch will have diverged from its rewritten twin by every commit | No agent commits during the freeze; after it, every agent re-creates its worktree, never merges an old-id branch into a new one |
|
||||
| Open pull requests, if any | Their base and head ids vanish | None open today (the project merges by hand); check `gh pr list` before the freeze |
|
||||
| The Vercel GitHub integration (`igneum` project, deploys on push to master) | The integration links by repository id, not by commit, so it survives a force-push; the first push of the rewritten `master` triggers one deploy of the same site (the public tree is unchanged by the pass except the dropped `site/ledger.html`, already a 307 redirect) | Watch the deploy; nothing to relink. If the repository is re-created instead (section 5, option B), the integration is re-linked once in the Vercel project settings |
|
||||
| The `windows-ci` and `ci` workflows | Run on the rewritten push like any push; the DL_TOKEN secret is a repository setting and survives | Re-set the secrets if the repository is re-created |
|
||||
| Old commit ids in documents (`docs/bench-log.md`, plans, the ledger) and in the public export | They name commits that will not exist; filter-repo writes `commit-map` (old id to new id) in `.git/filter-repo/` and rewrites ids it finds in commit messages, not in files | Keep `commit-map` with the private notes; the bench log keeps its short ids as historical labels (the public export already strips the history) |
|
||||
| GitHub's copies of the old objects | A force-push does not delete them from GitHub's object store; cached PR views, old commit URLs and forks keep serving them until GitHub runs a garbage collection, which support can be asked to do | Option B below removes the question |
|
||||
| The fork worktrees under `vendor/` | Separate repositories (`vendor/` is gitignored); untouched | Nothing |
|
||||
| The intake key and the dl token | Removing them from the history does not revoke them; every shipped package and every installed app carries the current key | Rotate first (the `.next` values exist since 4 October 19:25): new key in `relay/` and in `packaging/mac/packaged-config.sh`, repackage, republish; the old key keeps working for installed apps until they update, then dies |
|
||||
|
||||
## 5. The order of operations for the morning
|
||||
|
||||
1. Rotate the secrets: switch the relay and the intake to `log-intake-key.next`, the downloads folder to `dl-token.next`, repackage the Mac and Windows apps with the new values, publish, confirm an upload lands under the new key. Then the old values in the history are dead values.
|
||||
2. The owner renames the login `igneum-josh` (GitHub settings; the noreply id 337424239 stays), confirms the second login is no longer an organisation owner, and approves the public `CLAUDE.md` text (section 5 step 2).
|
||||
3. Freeze: every agent commits and pushes its branch, then stops; `gh pr list` must be empty; `git worktree list` is recorded.
|
||||
4. Mirror clone, run the pass (section 2) with the two extra lines from step 2 and the scrubbed `CLAUDE.md` blob; the greps of section 3 must all read 0; keep `commit-map`.
|
||||
5. Choose A or B. A: `git push --mirror` from the clone to the existing repository, then ask GitHub support to purge the unreachable objects. B (the route `docs/fud-fixes.md` step 4 prefers): create a fresh repository under the organisation, push the rewritten refs there, re-link Vercel and re-set the two secrets, archive the old repository private. B leaves no old object anywhere.
|
||||
6. Re-clone the main checkout from the new history; every agent re-creates its worktree from its rewritten branch.
|
||||
7. `TZ=UTC` on every path that commits: the agents' shells, the ship scripts, the relay; and `git config --global` cannot set a timezone, so the rule is in the environment. The CI identity grep and `git log --format='%ad' --date=raw | grep -c +0100` become the daily check (0 is the goal).
|
||||
8. The public export (`igneum-network/spec`) is unaffected: it carries no history from this repository.
|
||||
|
||||
## 6. What waits for the owner
|
||||
|
||||
| Decision | Options |
|
||||
|---|---|
|
||||
| The new login name | any handle without a name |
|
||||
| A or B in step 5 | B recommended |
|
||||
| The public `CLAUDE.md` text | section 5 step 2 of `docs/fud-fixes.md` |
|
||||
| The day | after step 1; before the public date in every case |
|
||||
170
docs/plans/release-0.3.6.md
Normal file
170
docs/plans/release-0.3.6.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Igneum Miner 0.3.6: the testnet adoption, staged 5 October 2026
|
||||
|
||||
Prepared by the integration agent on the morning of 5 October 2026 after the owner's decision. Nothing was deployed,
|
||||
published or merged to master. The app and document work sits on branch `testnet-adopt` (pushed to origin), worktree
|
||||
`/Users/joshm/Projects/igneum-wt-adopt`; the node work sits on the fork branch `release-0.3.6` (local only; the fork
|
||||
has no remote), worktree `vendor/igneum-node-036`. The cut is one command (section 4) after the inputs push of
|
||||
section 3, which must come first.
|
||||
|
||||
## 0. The decision (owner, 5 October 2026)
|
||||
|
||||
The proposed testnet identity (`docs/testnet/README.md`) and the proposed fee floors and prover-gas table
|
||||
(`docs/analysis/base-fee-floor.md`, spec 05 section 5.10) are ADOPTED as proposed. The three documents now say so;
|
||||
the fork's `FeeParams::CALIBRATED_V1` and `TESTNET_PARAMS` are unchanged from the proposal.
|
||||
|
||||
One rule the adoption added, so the live devnet is never forked by a node update: the devnet keeps its fee rules
|
||||
until a height switch says otherwise (section 5).
|
||||
|
||||
## 1. What is in
|
||||
|
||||
### 1a. The app repository (branch `testnet-adopt`, from origin/master 2054ae3 = the 0.3.5 cut)
|
||||
|
||||
| Merged | Branch head | What it carries | Conflicts and how they were resolved |
|
||||
|---|---|---|---|
|
||||
| `origin/testnet-prep` | beed743 (3 commits over e22068f) | `docs/testnet/README.md`, `docs/analysis/base-fee-floor.md`, spec 05 section 5.10, `docs/plans/history-rewrite.md` (G14, not acted on here), G13 signed build inputs (`app/igneum-app/src/inputs.rs`, `igneum-ota-sign sign-inputs` / `verify-inputs`, `packaging/windows/push-inputs.sh`, `inputs-manifest.sh`, `node-source.pin`, `test-inputs-signing.sh`, `fetch-ci-artifacts.sh --sign-manifest`, `.github/workflows/windows.yml`), `tools/ci/check-workflow-shell.mjs`, the testnet terms on the download section, `site/wallet.html`, the litepaper's app paragraph, the fast-time profile's `fees` object | four generated site files. `site/index.html`: the dev-fee sentence of 0.3.5 kept, testnet-prep's `#testnet-terms` card and wallet link kept (both sides of the download section); the inlined journey block is regenerated. `site/litepaper.html`: 0.3.5's two-paragraph dev-fee text kept (it is the fuller one; testnet-prep's one-sentence version dropped), testnet-prep's MetaMask and Igneum Wallet paragraph kept. `site/journey.json`: 0.3.5's log kept (the feed is the newest 40 entries; testnet-prep's older entries had already aged out of it). `site/sitemap.xml`: both `/miners` and `/wallet`. Then `node site/build.mjs` |
|
||||
|
||||
On top of the merge, in the same branch:
|
||||
|
||||
| Change | Where |
|
||||
|---|---|
|
||||
| "proposed" to "adopted 5 October 2026" with the sign-off noted, and the per-network fee rule written in | `docs/testnet/README.md`, `docs/analysis/base-fee-floor.md`, `docs/spec/05-fees-and-economics.md` (section 5.10 and the parameter table) |
|
||||
| `fees_v1_activation_daa: 0` added to the 60x fast-time profile (the fork's `fast_time_60x_file_is_the_devnet_at_60x` test wants every override field present) | `infra/fast-time/override-60x.json` |
|
||||
| this file | `docs/plans/release-0.3.6.md` |
|
||||
|
||||
### 1b. The node fork (branch `release-0.3.6` in `vendor/igneum-node-036`, from `release-0.3.5` 20139145)
|
||||
|
||||
| Fork merge | Head | What it carries | Conflicts |
|
||||
|---|---|---|---|
|
||||
| `testnet-params` (worktree `vendor/igneum-node-testnet`, forked from `finality-fixes` 6aa69a45) | 11e86144 | `consensus/core/src/fees.rs` (`FeeParams`, `PgasTable`, `CALIBRATED_V1`, `PROTOTYPE`), `Params.fees` and the override file's `fees` object, the testnet identity (`igneum-testnet-1`, chain id 4462, ports 268xx, the frozen genesis, `FinalityParams::MAINNET`, every switch at 0), the override file refused on the testnet, the execution layer reading the installed set (`B_p`, `S_p`, the intrinsic, modexp, the floors) | `consensus/core/src/config/params.rs`: both methods kept (`apply_env_pow_schedule` from G12 and `install_fee_params`); the M31 comment on `max_coinbase_payload_len` kept. `kaspad/src/daemon.rs`: the 0.3.5 start-up block kept (environment schedule on devnet and simnet, `install_pow_schedule`, the digest line) and `install_fee_params` with its own print added after it |
|
||||
|
||||
On top of the merge, the fee height switch (section 5), in the same branch:
|
||||
|
||||
| Change | Where |
|
||||
|---|---|
|
||||
| `FeeParams::DEVNET` and `SIMNET` = `PROTOTYPE`; `TESTNET` and `MAINNET` = `CALIBRATED_V1`. `FeeSchedule { base, v1_activation_daa }` with `at(daa)`; `install_fee_params(base, switch)`; `fee_params_at(daa)` replaces `fee_params()` | `consensus/core/src/fees.rs` |
|
||||
| `Params.fees_v1_activation_daa` (devnet and simnet `u64::MAX`, testnet and mainnet 0), `OverrideParams.fees_v1_activation_daa`, the digest rule of section 5, four new or changed tests | `consensus/core/src/config/params.rs` |
|
||||
| the switch and a `fees` object printed from the override file; the installed schedule printed with the network | `kaspad/src/daemon.rs` |
|
||||
| `shard_proving_gas_budget_at(daa)` | `consensus/core/src/proving.rs` |
|
||||
| every reader takes a DAA score; `execute_segment` picks the set by the block's DAA score and raises the carried base fees to its floors; the inspector carries the block's pgas table (modexp reads it); `next_base_fee` takes floor and denominator; the pool's `add` and `select` and every RPC quote use the tip's DAA score plus one; the shard plan uses the segment's; one new executor test | `igneum/exec/src/{config,executor,pgas,pool,proving,rpc,service}.rs` |
|
||||
|
||||
### 1c. What is out
|
||||
|
||||
| Branch | Why |
|
||||
|---|---|
|
||||
| fork `miner-latency` (0f88b6d6, `vendor/igneum-node-latency`, one commit over `devnet-v4`) and the app repository's local `miner-latency` (dd47ff3: `tools/miner-latency/run.mjs`, its plan and bench-log entries; never pushed to origin) | see section 2 |
|
||||
|
||||
## 2. The miner-latency decision
|
||||
|
||||
(filled in below once the merged tree's miner tests and the 3-node fast-time run have been read)
|
||||
|
||||
## 3. Test results, with the command
|
||||
|
||||
Every build ran through the main checkout's lock, `/Users/joshm/Projects/igneum/tools/lock/with-lock.sh build`, at
|
||||
`nice -n 19` with `-j 4`. The fork's builds used `CARGO_TARGET_DIR=vendor/igneum-node/target-036`, an APFS clone of
|
||||
`target-release` (48 s to clone). The rustup cargo (1.99.0) on `~/.cargo/bin`.
|
||||
|
||||
### 3a. The app repository (worktree `igneum-wt-adopt`)
|
||||
|
||||
| Check | Command | Result |
|
||||
|---|---|---|
|
||||
| Site | `node site/build.mjs` | built: bench, journey (40 entries), index, litepaper, live, evidence, wallet, 404, miners (6 rows) |
|
||||
| Links | `node tools/ci/link-check.mjs` | 324 internal links across 8 pages, 0 broken |
|
||||
| Workflow shell | `node tools/ci/check-workflow-shell.mjs` | self-test fires on the fixture; 11 run blocks in 2 workflows, 25 `.ps1` files, 0 findings |
|
||||
| Signed inputs | `packaging/windows/test-inputs-signing.sh` with `igneum-ota-sign` built from this tree (`cargo build --release -j 4 --bin igneum-ota-sign`, 8 s with the cloned target) | 16 passed, 0 failed: the four positive cases, the ten refusals (changed zip byte, changed manifest byte, changed unpacked file, unlisted file, missing file, wrong pin, short pin, another key, the embedded key against the throwaway signature, the two sign-time refusals), and the Mac's real OTA key verified by the key compiled into the app |
|
||||
| Shell parses | `bash -n` on `push-inputs.sh`, `fetch-ci-artifacts.sh`, `inputs-manifest.sh`, `test-inputs-signing.sh` | ok |
|
||||
| JavaScript parses | `node --check` on `tools/ci/check-workflow-shell.mjs`, `tools/ci/link-check.mjs`, `site/build.mjs` | ok |
|
||||
| PowerShell parse rule (no `$var:` inside double quotes) | the 0.3.5 scanner over every tracked `.ps1` and the PowerShell strings in `ota.rs`, `jobrun.rs`, `jobbuild.rs` | the 3 baseline hits of 0.3.5 only (two comments in `check-ps51.ps1` that quote the rule; `ota.rs` line 1190, the Mac helper's bash); testnet-prep changed no `.ps1` |
|
||||
|
||||
### 3b. The node fork (worktree `vendor/igneum-node-036`)
|
||||
|
||||
(filled in below)
|
||||
|
||||
## 4. The morning cut, in order
|
||||
|
||||
### 4a. The signed inputs come first (G13)
|
||||
|
||||
The workflow and `push-inputs.sh` changed as a pair. From this tree on, `.github/workflows/windows.yml` fetches
|
||||
`payload-inputs.json.sig` from the downloads host and refuses to build until `igneum-ota-sign verify-inputs embedded`
|
||||
accepts the manifest's signature, the zip's sha256, every unpacked file and the node commit pinned in
|
||||
`packaging/windows/node-source.pin` in the commit it builds. The downloads host holds no signature today (0.3.5
|
||||
pushed an unsigned `inputs.json` and a bare `.sha256`), and the pin in this tree still reads 6aa69a45 (the 0.3.4
|
||||
node, written by the old script). So the FIRST push of the new workflow would fail at "payload inputs" unless the
|
||||
inputs are pushed from this tree before the workflow runs:
|
||||
|
||||
```
|
||||
cd /Users/joshm/Projects/igneum-wt-adopt
|
||||
gh auth switch --user igneum-josh
|
||||
packaging/windows/test-inputs-signing.sh # 16 of 16, with the real key at the end
|
||||
IGNEUM_NODE_SRC=vendor/igneum-node-036 IGNEUM_WIN_RELEASE=<the 0.3.6 Windows release folder> \
|
||||
packaging/windows/push-inputs.sh --deploy # signs payload-inputs.json, writes node-source.pin
|
||||
git add packaging/windows/node-source.pin && git commit -m "inputs: pin node <commit> for 0.3.6"
|
||||
```
|
||||
|
||||
What the workflow needs, and where it comes from:
|
||||
|
||||
| Need | Source |
|
||||
|---|---|
|
||||
| `DL_TOKEN` repository secret | already set for 0.3.5 (`gh secret set DL_TOKEN < ~/.config/igneum/dl-token`); the workflow fetches the zip, the manifest and the signature with it |
|
||||
| the embedded public key | compiled into the app (`app/igneum-app/src/manifest.rs`); `igneum-ota-sign embedded` prints it; `push-inputs.sh` refuses to sign if `~/.config/igneum/ota-signing-key.pub` is not that key |
|
||||
| `~/.config/igneum/ota-signing-key` (0600) and `.pub` | the OTA key the apps already trust; signs the manifest on the Mac |
|
||||
| `packaging/windows/node-source.pin` | written by `push-inputs.sh`, committed with the push; the runner compares it with the manifest's `node_source_commit` |
|
||||
| the Windows node exes | `IGNEUM_WIN_RELEASE` names the folder with `igneumd.exe` and `igneum-miner.exe` built from the fork commit the pin names (the 0.3.6 cross-build; `proto-cuda/windows-node/cross-build.sh` as in 0.3.5) |
|
||||
|
||||
Then the update manifest is signed only on request: `packaging/windows/fetch-ci-artifacts.sh --sign-manifest <run-id>
|
||||
--deploy` after the run is green, which re-verifies the run's inputs artifact against the key and the pin at the
|
||||
run's commit before `publish-manifest.sh` runs.
|
||||
|
||||
### 4b. The merge and the ship
|
||||
|
||||
From the MAIN checkout, on master, after 4a's pin commit is on `testnet-adopt`:
|
||||
|
||||
```
|
||||
cd /Users/joshm/Projects/igneum
|
||||
gh auth switch --user igneum-josh
|
||||
git fetch origin && git checkout master && git pull --ff-only
|
||||
git merge --ff-only origin/testnet-adopt # testnet-adopt contains master 2054ae3
|
||||
node tools/ship-app.mjs --check # 0.3.5 in all 6 files
|
||||
node tools/ship-app.mjs 0.3.6 \
|
||||
--node vendor/igneum-node-036 \
|
||||
--win-release <the 0.3.6 Windows release folder> \
|
||||
--mac-release <the 0.3.6 Mac release folder> \
|
||||
--notes "Testnet identity and the adopted fee table in the node (devnet unchanged until the fee switch), signed build inputs"
|
||||
```
|
||||
|
||||
The 0.3.6 binaries are NOT built yet (this plan stops at the suites; section 3b says what was and was not built).
|
||||
Build them as 0.3.5's section 4a did: Mac `cargo build --release -j 4 -p kaspad -p igneum-miner --features
|
||||
kaspad/igneum-pow` with `CARGO_TARGET_DIR=vendor/igneum-node/target-036`; Windows through
|
||||
`proto-cuda/windows-node/cross-build.sh`; Linux through `infra/cross/build-linux.sh`. The push to master triggers
|
||||
`windows.yml`, which now verifies the inputs of 4a before it builds the app.
|
||||
|
||||
## 5. The devnet rollout of the fee floor: a height switch
|
||||
|
||||
The live devnet runs the prototype fee set (`B_p` 30,000,000, 1 gwei floors, intrinsic 200). A 0.3.6 node on the
|
||||
devnet runs exactly that until told otherwise, so 0.3.5 and 0.3.6 nodes build and accept the same segments through
|
||||
the whole rolling update. The adopted set reaches the devnet by `fees_v1_activation_daa`, the same pattern as
|
||||
`difficulty_v2_activation_daa` (33,000, 4 October 2026):
|
||||
|
||||
| Step | What | Check |
|
||||
|---|---|---|
|
||||
| 1 | Ship 0.3.6 (section 4). Every node updates with the switch at its default, never. The consensus digest does not move (the fee fields enter the digest only once the switch or the set leaves the 0.3.5 state), so 0.3.5 and 0.3.6 peers keep handshaking | `Consensus params digest: 9409dedac4bf...` on a node with no override file, the same line 0.3.5 printed; the live devnet nodes (override file with the difficulty and finality switches) print their own unchanged value |
|
||||
| 2 | Wait until every devnet node is on 0.3.6: the console's Machines card and the user agents in the peer list (`igneumd/2.1.0-<0.3.6 fork commit>`) | no `-20139145` user agent left |
|
||||
| 3 | Pick the switch height: a DAA score at least 24 hours ahead (86,400 blocks), on a round number, announced in the engineering log and the app's update note | |
|
||||
| 4 | Publish the switch with the other switches in the update manifest's `consensus.override` (`packaging/ota/publish-manifest.sh --override '{"difficulty_v2_activation_daa": 33000, ..., "fees_v1_activation_daa": N}' --deploy`), and add the line to every hand-run node's override file. The digest moves the moment a node restarts with it; a node that has not restarted is refused by those that have, which is why the restart must sweep every node before N | `Calibrated v1 fees from the override file: ... from DAA score N` in every node's start-up log; one digest across the peer list |
|
||||
| 5 | At N: the first chain block at or above N meters with v1 (intrinsic 300, `B_p` 120,000, `S_p` 30,000, modexp 10 + 1 per 10 bytes) and its base fees jump to 100 gwei per gas and 10,000 gwei per pgas. Nothing resets; history stays | `igneum_getBudgets` returns `provingGasLimit` 120,000 and the two base fees at the floors; `eth_getBlockByNumber` of the switch block shows `provingBaseFeePerGas` 0x9184e72a000 |
|
||||
| 6 | The prover's mirror of the table (`proving/igneum-prove/core/src/config.rs`, `pgas.rs`: prototype values at b7fca5a0) must carry v1 before N, or every shard statement after N differs from the node's plan. This is the one change this plan does not make; it re-cuts every fixture at `S_p` 30,000 and changes the guest program id | the fixtures and the guest id in `docs/analysis/base-fee-floor.md` section 4 |
|
||||
|
||||
Why a switch and not a fresh chain: the devnet has 12 cloud nodes, three PCs and outside machines on it, with the
|
||||
difficulty v2 rollout as the precedent that a height switch over a live chain works. Why not the override file's
|
||||
`fees` object: that changes the rules of every block including the past, so a node restarted with it could not
|
||||
replay its own history.
|
||||
|
||||
## 6. Open after this plan
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| `--netsuffix` default 1 under `--testnet` | one line in `kaspad/src/args.rs`, not done; `igneumd --testnet --netsuffix 1` until then |
|
||||
| the testnet genesis message | reads `proposed, not final` (the proposal's words, now part of the hashed genesis 52a3e6a9...). Adopted as computed. If the owner wants the words changed, it is one `print_genesis_hashes` run, new hash and merkle root in `genesis.rs`, `test_genesis_hashes` and the README, and it must happen before the first public node, never after |
|
||||
| the prover's table mirror | section 5 step 6 |
|
||||
| seed nodes, public RPC, the explorer, the app's testnet build | `docs/testnet/README.md` section 5, unchanged |
|
||||
| G14 history rewrite | `docs/plans/history-rewrite.md`, merged as a plan, not acted on |
|
||||
| the inputs push (4a) and the 0.3.6 binaries | not done here |
|
||||
|
|
@ -82,6 +82,37 @@ Designed, Open (O-5.3). Each block carries a bitfield; bit b set means "this blo
|
|||
| Upgrade window, activation delay | not set | Open (O-5.3) |
|
||||
| Shard assignment | sortition, 8 provers, 10-s window, no bond (section 7.2) | Designed (3 October 2026) |
|
||||
| External job bond, claim timeout | not set | Open (O-5.6) |
|
||||
| Proving-cost budget per block | from the phase 2 measurement | Target |
|
||||
| Proving-cost budget per block `B_p` | 120,000 pgas (section 5.10) | Adopted (5 October 2026, as proposed on 4 October 2026; was Target) |
|
||||
| Shard budget `S_p` | 30,000 pgas (section 5.10) | Adopted (5 October 2026, as proposed on 4 October 2026) |
|
||||
| Base-fee floors `f_e`, `f_p` | 100 gwei per gas, 10,000 gwei per pgas (section 5.10) | Adopted (5 October 2026, as proposed on 4 October 2026) |
|
||||
| pgas table | version 1: intrinsic 300, modexp 10 + 1 per 10 bytes, other entries prototype (section 5.10) | Adopted (5 October 2026, as proposed on 4 October 2026) |
|
||||
| Emission to any treasury | 0 | Designed |
|
||||
| Protocol fee to any team, foundation or fund | 0 | Designed (3 October 2026) |
|
||||
|
||||
## 5.10 Base-fee floors, the proving budget and the pgas table (Adopted 5 October 2026)
|
||||
|
||||
Proposed on 4 October 2026 and adopted by the owner on 5 October 2026, as proposed (the sign-off is recorded in
|
||||
`docs/plans/release-0.3.6.md`). The arithmetic is in `docs/analysis/base-fee-floor.md`; the values are implemented
|
||||
in the node fork (`consensus/core/src/fees.rs`, `FeeParams::CALIBRATED_V1`, carried by `Params.fees` per network
|
||||
and by the override file; merged into the fork's `release-0.3.6`). The testnet and the mainnet run them from
|
||||
genesis. The devnet and the simnet keep the prototype values (both base fees 1 gwei with a 1 gwei floor, `B_p` =
|
||||
`B_e` = 30,000,000, intrinsic 200, modexp 1,000 + 10 per byte; `FeeParams::PROTOTYPE`) until the
|
||||
`fees_v1_activation_daa` height switch, carried by the override file and the consensus digest, moves them: chain
|
||||
blocks at or above that DAA score meter with the adopted table, budgets and floors, and the first such block raises
|
||||
both base fees to the floors.
|
||||
|
||||
| Parameter | Adopted | Basis |
|
||||
|---|---|---|
|
||||
| pgas unit | 1 pgas = 1,000 reference SP1 cycles | unchanged |
|
||||
| Intrinsic pgas per transaction | 300 | measured upper bound: 1,400 to 1,600 cycles per prototype pgas on a plain-transfer shard (bench-log, 4 October) |
|
||||
| modexp entry | 10 + 1 per 10 input bytes | measured: 9 cycles per prototype pgas on a modexp-heavy shard, the prototype entry about 100x its cost |
|
||||
| Other opcode and precompile entries | prototype shapes, table version 1 | not yet measured |
|
||||
| Shard budget `S_p` | 30,000 pgas (30 M cycles) | about 5.5 s compressed on one RTX 5090 (half the measured 60 M-cycle shard at 10.9 s, approximate); about 20 s on a 12 GB card (approximate) |
|
||||
| Block proving budget `B_p` | 120,000 pgas (4 x `S_p`) | 400 transfers per block; a four-shard block proves in about 8 s on four RTX 5090s (approximate) |
|
||||
| Execution base-fee floor `f_e` | 100 gwei per gas | a full block burns 3 IGN, 9.5% of the year-one subsidy; 259,200 IGN per day |
|
||||
| Proving base-fee floor `f_p` | 10,000 gwei per pgas | 230x the proving electricity per pgas at an assumed $0.10 per IGN and $0.15 per kWh |
|
||||
| Initial base fees | the floors | |
|
||||
| Adjustment | EIP-1559 toward half the limit, denominator 8, both dimensions | unchanged |
|
||||
| A plain transfer at the floor | 21,000 x 100 gwei + 300 x 10,000 gwei = 0.0051 IGN | under $0.01 for any token price up to $1.96 (assumption, not a forecast) |
|
||||
|
||||
The floors and `B_p` are parameters the genesis rules leave to miners (5.5): they move by 60% signalling.
|
||||
|
|
|
|||
83
docs/testnet/README.md
Normal file
83
docs/testnet/README.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Igneum public testnet: identity, parameters and reset policy (ADOPTED 5 October 2026)
|
||||
|
||||
Every value in this file was proposed on the night of 4 October 2026 and ADOPTED by the owner on 5 October 2026,
|
||||
as proposed (sign-off recorded in `docs/plans/release-0.3.6.md`). The genesis below is the one the proposal
|
||||
computed; its values did not change at the sign-off, so the hash stands. The code was written on the node fork's
|
||||
branch `testnet-params` (worktree `vendor/igneum-node-testnet`, forked from `finality-fixes` at `6aa69a45`) and
|
||||
merged on 5 October 2026 into the fork's `release-0.3.6` (worktree `vendor/igneum-node-036`), with one change the
|
||||
sign-off added: the devnet and the simnet keep the prototype fee set until a height switch
|
||||
(`fees_v1_activation_daa`) or a `fees` object in the override file moves them; the testnet and the mainnet carry
|
||||
calibrated v1 from genesis (section 3). Nothing is deployed; the testnet has no seed nodes yet (section 5).
|
||||
|
||||
## 1. Identity
|
||||
|
||||
| Field | Devnet (live today) | Testnet (adopted) | Where |
|
||||
|---|---|---|---|
|
||||
| Network id (handshake string, data directory) | `igneum-devnet` | `igneum-testnet-1` | `consensus/core/src/network.rs`, `NetworkId::to_prefixed` |
|
||||
| Network type and suffix | Devnet, none | Testnet, suffix 1 | `config/params.rs`, `From<NetworkId>`: only suffix 1 resolves; any other suffix is refused |
|
||||
| EVM chain id | 4463 | 4462 | `consensus/core/src/evm.rs`, `evm_chain_id` (mainnet 4461) |
|
||||
| Address prefix | `igneumdev` | `igneumtest` | `crypto/addresses` |
|
||||
| gRPC port | 26610 | 26810 | `network.rs`, `default_rpc_port` |
|
||||
| P2P port | 26611 | 26811 | `network.rs`, `default_p2p_port` (a later suffix takes the next port) |
|
||||
| wRPC Borsh, JSON | 27610, 28610 | 27810, 28810 | `network.rs` |
|
||||
| EVM JSON-RPC port | 26790 | 26890 | `igneum/exec/src/config.rs`, `default_evm_rpc_port` |
|
||||
| DNS seeders | none | none (the list is filled when the seed nodes exist, `docs/plans/seed-nodes.md`) | `TESTNET_PARAMS.dns_seeders` |
|
||||
| Override file (`--override-params-file`) | allowed | refused, as on mainnet | `kaspad/src/daemon.rs` |
|
||||
| `IGNEUM_POW_*` environment | ignored by the node from this branch (G12: the params' schedule is installed on every start) | ignored | `kaspad/src/daemon.rs` |
|
||||
|
||||
Start a node on it: `igneumd --testnet --netsuffix 1` (the flag `--testnet` is "Use the Igneum test network";
|
||||
`--netsuffix` defaults to 10 in `kaspad/src/args.rs` and must be set to 1 until the default is changed, which is
|
||||
one line and waits for the sign-off).
|
||||
|
||||
## 2. Genesis
|
||||
|
||||
| Field | Value | Note |
|
||||
|---|---|---|
|
||||
| Timestamp | 1,791,158,400,000 ms = 2026-10-05T00:00:00Z (`0x1a1095c3400`) | frozen; adopted 5 October 2026 |
|
||||
| Bits | `0x1d100000` (2^28 expected hashes per block) | the devnet's launch difficulty, sized for a few hundred MH/s; the DAA takes over after 150 samples (600 blocks); re-size to the announced launch fleet |
|
||||
| Nonce, DAA score | 0, 0 | |
|
||||
| UTXO commitment | empty | |
|
||||
| Coinbase payload message | `igneum-testnet-1 \| 2026-10-05 \| proposed, not final \| coins here have no value \| resets are announced` | after the OP-FALSE script, as the devnet's `igneum-devnet`. The words "proposed, not final" are the proposal's and are part of the hashed genesis; the sign-off adopted the genesis as computed. Changing the message is one `print_genesis_hashes` run and a new hash, and must happen before the first public node starts, never after (`docs/plans/release-0.3.6.md`, section 6) |
|
||||
| Hash | `52a3e6a9ddd79d603ff9e3a27487fb5d0ca5ca6f633fe20ca727e1faa355fc7e` | computed 4 October 2026 by `print_genesis_hashes` on the branch (pinned by `test_genesis_hashes`, re-run green on `release-0.3.6` on 5 October 2026); changes with any field above |
|
||||
| Merkle root | `3060aab78494de3cbda68619c0132300aa486dc3153dbb78b50a430f053b3a84` | same |
|
||||
|
||||
## 3. Consensus parameters
|
||||
|
||||
| Parameter | Testnet (adopted) | Devnet today | Why |
|
||||
|---|---|---|---|
|
||||
| Block rate | 1 per second | 1 per second | spec 02 |
|
||||
| Difficulty rule | Igneum dual-lane, v2 from genesis | dual-lane, v2 from DAA 33,000 | a fresh chain has no pre-switch history |
|
||||
| Finality parameters | `FinalityParams::MAINNET`: 30-day weight window (2,592,000 DAA), dust 100, presence 240, 8 aggregators, 30-day equivocation ban, min DAA 2,592,000, certificate fold 6 | `DEVNET`: 2-hour window, dust 5, presence 20, fold 3 | the testnet runs the rule the mainnet will run; the first lock needs 30 days of weight, which is the point of a testnet |
|
||||
| Finality rule v3 | from genesis | from the override file | fresh chain |
|
||||
| Proving v0 payouts | from genesis | from the override file | fresh chain |
|
||||
| PoW schedule | epoch 3,600 DAA, lead 600, day 86,400,000 ms (the defaults) | same | |
|
||||
| Coinbase payload limit | 16,384 (the finality section) | same | |
|
||||
| Fees | `FeeParams::CALIBRATED_V1` from genesis (`fees_v1_activation_daa` 0; `docs/analysis/base-fee-floor.md`): `B_p` 120,000 pgas, `S_p` 30,000, intrinsic 300, modexp 10 + 1 per 10 bytes, floors 100 gwei per gas and 10,000 gwei per pgas | `FeeParams::PROTOTYPE` (`B_p` 30 M, 1 gwei), kept on the 0.3.6 node so the live chain does not change rules between builds; moves to v1 by the `fees_v1_activation_daa` height switch in the override file (`docs/plans/release-0.3.6.md`, section 5) | spec 05 section 5.10 |
|
||||
| Emission | the mainnet schedule: 31.69 IGN per block in year one, halving every two years, cap 4 billion | same | the testnet coins have no value whatever the schedule says |
|
||||
|
||||
Everything else (mass limits, GHOSTDAG k, merge depth, pruning) is the devnet's set, unchanged.
|
||||
|
||||
## 4. Reset policy
|
||||
|
||||
| Rule | Adopted 5 October 2026 |
|
||||
|---|---|
|
||||
| Coins | Testnet IGN has no value, cannot be bought, sold or redeemed, and is not a claim on anything at mainnet. Mainnet starts from an empty genesis. No airdrop, no points, no promise tied to a testnet balance |
|
||||
| When the chain resets | When a consensus rule changes (the lottery hash, the difficulty rule, finality, the fee table, the execution rules) and a height switch is not the right tool; or when the chain is broken beyond a height switch |
|
||||
| Notice | At least N = 7 days ahead, on igneum.network (the download page and the live page), in the app through the update manifest's note, and in the engineering log. A reset without notice is a bug report, not a policy |
|
||||
| What carries over | Nothing. Balances, contracts, nonces and history start again. Addresses stay valid (an address is a key) |
|
||||
| Identity after a reset | A consensus-changing reset takes the next suffix (`igneum-testnet-2`, chain id unchanged at 4462, P2P port 26812), so a node on the old rules never completes a handshake with the new chain |
|
||||
| The devnet | Keeps resetting without notice; it is a developer network. Its chain id 4463 stays separate |
|
||||
| Who decides | The project, by the sign-off that this file records; the parameters the genesis rules leave to miners (`B_p`, the floors) move by 60% signalling once the testnet has miners (spec 5.5) |
|
||||
|
||||
## 5. What stands between this file and a public testnet
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| Sign-off of every value above | done: adopted by the owner on 5 October 2026, as proposed |
|
||||
| `--netsuffix` default 1 under `--testnet` | one line in `kaspad/src/args.rs`, still to do (not in the 0.3.6 merge; `--netsuffix 1` must be passed until then) |
|
||||
| Seed nodes and the DNS seeder list | `docs/plans/seed-nodes.md`; the list in `TESTNET_PARAMS` is empty on purpose |
|
||||
| The public RPC and explorer | `site/wallet.html` carries a placeholder RPC URL until they exist |
|
||||
| The prover's table mirror and fixtures | `docs/analysis/base-fee-floor.md` section 4 |
|
||||
| The app's testnet build | the app's packaged config names the network; `igneum-testnet-1` needs the network and ports there (`app/igneum-app/src/config.rs`, `Runtime.network`) |
|
||||
| The params digest in the handshake (X18) | separate work, before the testnet |
|
||||
| Terms on the download page | `site/index.html#testnet-terms`, on branch `testnet-prep` with this file |
|
||||
|
|
@ -53,5 +53,7 @@
|
|||
"pow_day_ms": 1440000,
|
||||
"difficulty_v2_activation_daa": 18446744073709551615,
|
||||
"proving_v0_activation_daa": 18446744073709551615,
|
||||
"finality_v3_activation_daa": 18446744073709551615
|
||||
"finality_v3_activation_daa": 18446744073709551615,
|
||||
"fees_v1_activation_daa": 0,
|
||||
"fees": {"pgas": {"version": 1, "cycles_per_pgas": 1000, "intrinsic_pgas_per_tx": 300, "modexp_base": 10, "modexp_per_byte_numer": 1, "modexp_per_byte_denom": 10}, "block_proving_gas_limit": 120000, "shard_proving_gas_budget": 30000, "min_execution_base_fee_wei": 100000000000, "min_proving_base_fee_wei": 10000000000000, "initial_execution_base_fee_wei": 100000000000, "initial_proving_base_fee_wei": 10000000000000, "base_fee_change_denominator": 8}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
# Pulls the Windows installer and payload from the latest green run of .github/workflows/windows.yml on master and
|
||||
# copies them into the downloads folder (dl/<token>/), where the other packages live. Run on the Mac:
|
||||
# Pulls the Windows installer and payload from a green run of .github/workflows/windows.yml on master and copies
|
||||
# them into the downloads folder (dl/<token>/), where the other packages live. Run on the Mac:
|
||||
#
|
||||
# packaging/windows/fetch-ci-artifacts.sh [--deploy] [run-id]
|
||||
# packaging/windows/fetch-ci-artifacts.sh [--deploy] [--sign-manifest] [run-id]
|
||||
#
|
||||
# Without --deploy it prints the deploy command for the main session to run; with --deploy it deploys the folder with
|
||||
# the Vercel CLI itself. A run id (gh run list) picks a specific run instead of the latest green one.
|
||||
# The installer also goes into the over-the-air update manifest (packaging/ota/publish-manifest.sh, Windows entry;
|
||||
# OTA_NOTES= for the changelog line, OTA_SKIP=1 to leave the manifest alone), so the deploy ships both.
|
||||
#
|
||||
# The over-the-air update manifest (packaging/ota/publish-manifest.sh, Windows entry) is NOT touched unless
|
||||
# --sign-manifest is given (review round 4, R4.5.2, ledger G13: signing used to be automatic from "the latest green
|
||||
# run"). --sign-manifest needs an explicit run id, and before it signs anything it downloads that run's
|
||||
# igneum-windows-inputs artifact (the payload-inputs.json the runner verified, its signature and the runner's record)
|
||||
# and re-verifies on this Mac: the Ed25519 signature against ~/.config/igneum/ota-signing-key.pub, the pinned node
|
||||
# commit against packaging/windows/node-source.pin AT THE RUN'S COMMIT, the run's branch (master) and event (push or
|
||||
# workflow_dispatch), and that the runner's record names this run, this commit and this key. Any failure stops
|
||||
# before the signature. OTA_NOTES= sets the changelog line (default: the version and the run id).
|
||||
# Reads ~/.config/igneum/dl-token, ~/.config/igneum/dlsite-dir (IGNEUM_DLSITE overrides) and the gh login, which must
|
||||
# be igneum-josh (gh auth switch --user igneum-josh).
|
||||
set -euo pipefail
|
||||
REPO="igneum-network/igneum"
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
DEPLOY=0
|
||||
SIGN=0
|
||||
RUN_ID=""
|
||||
for a in "$@"; do
|
||||
case "$a" in --deploy) DEPLOY=1 ;; *) RUN_ID="$a" ;; esac
|
||||
case "$a" in --deploy) DEPLOY=1 ;; --sign-manifest) SIGN=1 ;; --*) echo "unknown flag $a" >&2; exit 2 ;; *) RUN_ID="$a" ;; esac
|
||||
done
|
||||
if [ "${OTA_SKIP:-}" = 0 ]; then SIGN=1; fi # the old spelling; OTA_SKIP=1 (the default now) leaves the manifest alone
|
||||
if [ "$SIGN" = 1 ] && [ -z "$RUN_ID" ]; then
|
||||
echo "--sign-manifest needs the run id it signs (gh run list --repo $REPO --workflow windows.yml); the latest green run is never signed by default" >&2
|
||||
exit 2
|
||||
fi
|
||||
TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
||||
DLSITE="${IGNEUM_DLSITE:-}"
|
||||
[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true
|
||||
|
|
@ -32,6 +47,9 @@ if [ -z "$RUN_ID" ]; then
|
|||
[ -n "$RUN_ID" ] && [ "$RUN_ID" != "null" ] || { echo "no green windows.yml run on master yet" >&2; exit 1; }
|
||||
fi
|
||||
gh run view "$RUN_ID" --repo "$REPO" --json headSha,displayTitle,updatedAt,url,conclusion --jq '"run \(.url)\n\(.displayTitle)\n\(.headSha[0:12]) \(.updatedAt) \(.conclusion)"'
|
||||
RUN_JSON="$(gh run view "$RUN_ID" --repo "$REPO" --json headSha,headBranch,event,conclusion,status)"
|
||||
read -r HEAD_SHA HEAD_BRANCH RUN_EVENT RUN_CONCLUSION < <(printf '%s' "$RUN_JSON" | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["headSha"], r["headBranch"], r["event"], r["conclusion"])')
|
||||
[ "$RUN_CONCLUSION" = success ] || { echo "run $RUN_ID concluded '$RUN_CONCLUSION', not success" >&2; exit 1; }
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
gh run download "$RUN_ID" --repo "$REPO" --name igneum-windows-installer --name igneum-windows-payload --dir "$TMP"
|
||||
|
|
@ -49,11 +67,41 @@ ls -la "$DEST/$(basename "$SETUP")" "$DEST/igneum-windows-app.zip"
|
|||
# the console's Builds tab (relay/): one build event per fetched CI run; never fatal. CONSOLE_SKIP=1 leaves it to the
|
||||
# caller (tools/ship-app.mjs posts one item for the whole cut).
|
||||
[ "${CONSOLE_SKIP:-0}" = 1 ] || node "$(dirname "$0")/../../tools/console.mjs" post --kind build --title "Windows CI $(basename "$SETUP") fetched (run $RUN_ID)" --body "https://github.com/$REPO/actions/runs/$RUN_ID" >/dev/null 2>&1 || true
|
||||
# the over-the-air manifest (packaging/ota): the Windows entry for this installer; the Mac entry of the same version is
|
||||
# carried over. OTA_NOTES= sets the changelog line; OTA_SKIP=1 leaves the manifest alone.
|
||||
if [ "${OTA_SKIP:-0}" != 1 ]; then
|
||||
# the over-the-air manifest (packaging/ota): only with --sign-manifest, only for the named run, and only after the
|
||||
# run's verified inputs manifest re-verifies here (G13). The Mac entry of the same version is carried over.
|
||||
if [ "$SIGN" = 1 ]; then
|
||||
PUB="$HOME/.config/igneum/ota-signing-key.pub"
|
||||
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
|
||||
[ -f "$PUB" ] || { echo "no $PUB" >&2; exit 1; }
|
||||
[ -x "$SIGNER" ] || (cd "$ROOT/app/igneum-app" && nice -n 19 cargo build --release -j 4 --bin igneum-ota-sign --quiet)
|
||||
[ "$HEAD_BRANCH" = master ] || { echo "refusing to sign: run $RUN_ID is on branch '$HEAD_BRANCH', not master" >&2; exit 1; }
|
||||
case "$RUN_EVENT" in push|workflow_dispatch) ;; *) echo "refusing to sign: run $RUN_ID was triggered by '$RUN_EVENT'" >&2; exit 1 ;; esac
|
||||
INP="$(mktemp -d)"
|
||||
gh run download "$RUN_ID" --repo "$REPO" --name igneum-windows-inputs --dir "$INP" || { echo "refusing to sign: run $RUN_ID has no igneum-windows-inputs artifact (the run verified no inputs manifest)" >&2; exit 1; }
|
||||
IJSON="$(find "$INP" -name payload-inputs.json | head -1)"; ISIG="$(find "$INP" -name payload-inputs.json.sig | head -1)"; IREC="$(find "$INP" -name inputs-verified.json | head -1)"
|
||||
[ -n "$IJSON" ] && [ -n "$ISIG" ] && [ -n "$IREC" ] || { echo "refusing to sign: the inputs artifact is incomplete" >&2; ls -R "$INP" >&2; exit 1; }
|
||||
# the pin as it stood in the commit the runner built, from this clone (fetched if the commit is not here yet)
|
||||
git -C "$ROOT" cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null || git -C "$ROOT" fetch --quiet origin "$HEAD_SHA" || true
|
||||
PIN="$(git -C "$ROOT" show "$HEAD_SHA:packaging/windows/node-source.pin" 2>/dev/null | tr -d '[:space:]')"
|
||||
[ ${#PIN} = 40 ] || { echo "refusing to sign: commit ${HEAD_SHA:0:12} carries no packaging/windows/node-source.pin" >&2; exit 1; }
|
||||
"$SIGNER" verify-inputs "$PUB" "$IJSON" "$ISIG" --node-commit "$PIN" || { echo "refusing to sign: the run's inputs manifest does not verify against $PUB and the pin at ${HEAD_SHA:0:12}" >&2; exit 1; }
|
||||
FP="$("$SIGNER" fingerprint "$PUB" | sed -n 2p)"
|
||||
python3 - "$IREC" "$RUN_ID" "$HEAD_SHA" "$FP" <<'PYREC'
|
||||
import json, sys
|
||||
rec = json.load(open(sys.argv[1]))
|
||||
want = {"run_id": sys.argv[2], "head_sha": sys.argv[3], "key_fingerprint": sys.argv[4]}
|
||||
bad = [k for k, v in want.items() if str(rec.get(k, "")) != v]
|
||||
if bad:
|
||||
print("refusing to sign: the runner's record disagrees on " + ", ".join(f"{k} (record {rec.get(k)!r}, expected {want[k]!r})" for k in bad), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"inputs verified by the runner and again here: node commit {rec.get('node_commit')}, zip {rec.get('zip_sha256')}, key {rec.get('key_fingerprint')}")
|
||||
PYREC
|
||||
rm -rf "$INP"
|
||||
SETUP_VERSION="$(basename "$SETUP" | sed -n 's/^Igneum-Miner-Setup-\(.*\)\.exe$/\1/p')"
|
||||
"$(dirname "$0")/../ota/publish-manifest.sh" --version "$SETUP_VERSION" --win "$DEST/$(basename "$SETUP")" --notes "${OTA_NOTES:-Windows build $SETUP_VERSION from CI run $RUN_ID}" --no-deploy
|
||||
echo "signing the update manifest for Windows $SETUP_VERSION over run $RUN_ID (${HEAD_SHA:0:12})"
|
||||
"$HERE/../ota/publish-manifest.sh" --version "$SETUP_VERSION" --win "$DEST/$(basename "$SETUP")" --notes "${OTA_NOTES:-Windows build $SETUP_VERSION from CI run $RUN_ID}" --no-deploy
|
||||
else
|
||||
echo "update manifest untouched (pass --sign-manifest <run-id> to sign it after the inputs check)"
|
||||
fi
|
||||
if [ "$DEPLOY" = 1 ]; then
|
||||
(cd "$DLSITE" && npx vercel@latest --global-config "$HOME/.config/igneum/vercel" deploy --prod --yes 2>&1 | grep -v "$TOKEN" || true)
|
||||
|
|
|
|||
44
packaging/windows/inputs-manifest.sh
Executable file
44
packaging/windows/inputs-manifest.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
# The payload-inputs manifest writer, shared by push-inputs.sh (the real thing) and test-inputs-signing.sh (the
|
||||
# Mac-side test), so the test signs and verifies exactly the shape the runner sees. Format: app/igneum-app/src/inputs.rs
|
||||
# (`igneum-payload-inputs/1`): the zip's sha256 and size, every file inside the zip's folder with its sha256 and
|
||||
# size, the node fork commit (40 hex) and branch the exes came from, the main repository commit, the build time.
|
||||
#
|
||||
# source packaging/windows/inputs-manifest.sh
|
||||
# write_inputs_manifest <stage-dir> <zip> <node-commit-40hex> <node-branch> <repo-commit-40hex> <out.json>
|
||||
#
|
||||
# Sorted file names, two-space indentation, one entry per line: the bytes are what gets signed, so the writer is
|
||||
# deterministic for the same inputs.
|
||||
|
||||
inputs_sha256() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
||||
inputs_size() { stat -f %z "$1" 2>/dev/null || stat -c %s "$1"; }
|
||||
|
||||
write_inputs_manifest() {
|
||||
local stage="$1" zip="$2" node_commit="$3" node_branch="$4" repo_commit="$5" out="$6"
|
||||
[ -d "$stage" ] || { echo "write_inputs_manifest: no stage folder $stage" >&2; return 1; }
|
||||
[ -f "$zip" ] || { echo "write_inputs_manifest: no zip $zip" >&2; return 1; }
|
||||
case "$node_commit" in [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) [ ${#node_commit} = 40 ] || { echo "write_inputs_manifest: node commit is not 40 hex" >&2; return 1; } ;; *) echo "write_inputs_manifest: node commit is not 40 hex" >&2; return 1 ;; esac
|
||||
[ ${#repo_commit} = 40 ] || { echo "write_inputs_manifest: repo commit is not 40 hex" >&2; return 1; }
|
||||
{
|
||||
echo '{'
|
||||
echo ' "format": "igneum-payload-inputs/1",'
|
||||
echo " \"built_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
|
||||
echo " \"node_source_commit\": \"$node_commit\","
|
||||
echo " \"node_source_branch\": \"$node_branch\","
|
||||
echo " \"repo_commit\": \"$repo_commit\","
|
||||
echo " \"zip\": { \"sha256\": \"$(inputs_sha256 "$zip")\", \"bytes\": $(inputs_size "$zip") },"
|
||||
echo ' "files": {'
|
||||
local first=1 f name
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
name="$(basename "$f")"
|
||||
[ "$name" = ".DS_Store" ] && continue
|
||||
[ $first = 1 ] || echo ','
|
||||
first=0
|
||||
printf ' "%s": { "sha256": "%s", "bytes": %s }' "$name" "$(inputs_sha256 "$f")" "$(inputs_size "$f")"
|
||||
done < <(find "$stage" -maxdepth 1 -type f | LC_ALL=C sort)
|
||||
echo
|
||||
echo ' }'
|
||||
echo '}'
|
||||
} > "$out"
|
||||
}
|
||||
1
packaging/windows/node-source.pin
Normal file
1
packaging/windows/node-source.pin
Normal file
|
|
@ -0,0 +1 @@
|
|||
6aa69a45364b9b30a32695e33eb66f100c9be85f
|
||||
|
|
@ -8,14 +8,19 @@
|
|||
# What goes in (flat): igneumd.exe, igneum-miner.exe (vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/
|
||||
# release, IGNEUM_WIN_RELEASE overrides), the three mingw runtime DLLs, igneum-worker-cuda.exe with nvrtc64_*_0.dll,
|
||||
# nvrtc-builtins64_*.dll and the licence texts (proto-cuda/nvrtc, fetch-redist.sh + build-windows.sh),
|
||||
# igneum-worker-opencl.exe (proto-opencl), and inputs.json (sha256 and size of each file, the commits, the date).
|
||||
# The zip, its .sha256 and the .json land in the downloads folder (dl/<token>/) and the folder is deployed with the
|
||||
# Vercel CLI, exactly as the other packages are. The workflow fetches them with the DL_TOKEN repository secret and
|
||||
# refuses a zip whose sha256 does not match.
|
||||
# igneum-worker-opencl.exe (proto-opencl). Beside the zip: payload-inputs.json (the signed manifest, format
|
||||
# app/igneum-app/src/inputs.rs: the zip's sha256 and size, every file's sha256 and size, the node fork commit and
|
||||
# branch, the repository commit, the time) and payload-inputs.json.sig, its detached Ed25519 signature made on this
|
||||
# Mac with the OTA key (~/.config/igneum/ota-signing-key, the key the apps already trust). The workflow verifies the
|
||||
# signature with the public key compiled into the app BEFORE it builds anything, checks the zip and every unpacked
|
||||
# file against the manifest, and checks the node commit against packaging/windows/node-source.pin in the commit it
|
||||
# builds (review round 4, R4.5.2, ledger G13). This script writes the pin; commit it with the push.
|
||||
# The zip, the manifest, the signature and the pin's sibling payload-inputs.sha256 (kept for older checkouts of
|
||||
# the workflow) land in the downloads folder (dl/<token>/) and the folder is deployed with the Vercel CLI.
|
||||
#
|
||||
# Where things are read from (never in the repo): the token in ~/.config/igneum/dl-token, the downloads folder in
|
||||
# ~/.config/igneum/dlsite-dir (one line, the path of the dlsite directory; IGNEUM_DLSITE overrides), the Vercel login
|
||||
# in ~/.config/igneum/vercel.
|
||||
# in ~/.config/igneum/vercel, the signing key in ~/.config/igneum/ota-signing-key (0600) and its public half .pub.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
|
|
@ -50,38 +55,46 @@ if [ -f "$NVRTC_DIR/igneum-worker-cuda.exe" ]; then
|
|||
else echo "warning: no $NVRTC_DIR/igneum-worker-cuda.exe (run $NVRTC_DIR/build-windows.sh); the app will build the CUDA worker on the PC"; fi
|
||||
[ -f "$CL_WORKER" ] && cp "$CL_WORKER" "$STAGE/" || echo "warning: no $CL_WORKER"
|
||||
|
||||
# the manifest: what is in the zip, from where, when
|
||||
# IGNEUM_NODE_SRC names the worktree the exes were built from (default devnet-v4), for the manifest's commit field
|
||||
NODE_COMMIT="$(git -C "${IGNEUM_NODE_SRC:-$ROOT/vendor/igneum-node-v4}" rev-parse --short HEAD 2>/dev/null || git -C "$ROOT/vendor/igneum-node" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
REPO_COMMIT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
{
|
||||
echo '{'
|
||||
echo " \"built_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
|
||||
echo " \"node_source_commit\": \"$NODE_COMMIT\","
|
||||
echo " \"repo_commit\": \"$REPO_COMMIT\","
|
||||
echo ' "files": {'
|
||||
first=1
|
||||
for f in "$STAGE"/*; do
|
||||
name="$(basename "$f")"
|
||||
sum="$(shasum -a 256 "$f" | cut -d' ' -f1)"
|
||||
bytes="$(stat -f %z "$f")"
|
||||
[ $first = 1 ] || echo ','
|
||||
first=0
|
||||
printf ' "%s": { "sha256": "%s", "bytes": %s }' "$name" "$sum" "$bytes"
|
||||
done
|
||||
echo
|
||||
echo ' }'
|
||||
echo '}'
|
||||
} > "$STAGE/inputs.json"
|
||||
# the signer, built from the app crate (it includes src/manifest.rs and src/inputs.rs, so it signs what the runner verifies)
|
||||
KEY="$HOME/.config/igneum/ota-signing-key"
|
||||
PUB="$HOME/.config/igneum/ota-signing-key.pub"
|
||||
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
|
||||
[ -f "$KEY" ] && [ -f "$PUB" ] || { echo "no $KEY or $PUB (the OTA signing key; packaging/ota/publish-manifest.sh explains keygen)" >&2; exit 1; }
|
||||
if [ ! -x "$SIGNER" ]; then
|
||||
echo "building igneum-ota-sign"
|
||||
(cd "$ROOT/app/igneum-app" && nice -n 19 cargo build --release -j 4 --bin igneum-ota-sign --quiet)
|
||||
fi
|
||||
EMBEDDED="$("$SIGNER" embedded | head -1)"
|
||||
[ "$EMBEDDED" = "$(tr -d '[:space:]' < "$PUB")" ] || { echo "the public key in app/igneum-app/src/manifest.rs ($EMBEDDED) is not $PUB; the runner would refuse this signature" >&2; exit 1; }
|
||||
|
||||
# the manifest: what is in the zip, from where, when. IGNEUM_NODE_SRC names the worktree the exes were built from
|
||||
# (default devnet-v4); its full commit is pinned in the manifest and in packaging/windows/node-source.pin.
|
||||
NODE_SRC="${IGNEUM_NODE_SRC:-$ROOT/vendor/igneum-node-v4}"
|
||||
NODE_COMMIT="$(git -C "$NODE_SRC" rev-parse HEAD 2>/dev/null || true)"
|
||||
[ ${#NODE_COMMIT} = 40 ] || { echo "cannot read the node source commit from $NODE_SRC (set IGNEUM_NODE_SRC to the worktree the exes were built from)" >&2; exit 1; }
|
||||
NODE_BRANCH="$(git -C "$NODE_SRC" rev-parse --abbrev-ref HEAD 2>/dev/null || echo detached)"
|
||||
if [ -n "$(git -C "$NODE_SRC" status --porcelain --untracked-files=no 2>/dev/null)" ]; then
|
||||
echo "warning: $NODE_SRC has uncommitted changes; the pinned commit $NODE_COMMIT does not describe these exes exactly" >&2
|
||||
fi
|
||||
REPO_COMMIT="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)"
|
||||
[ ${#REPO_COMMIT} = 40 ] || { echo "cannot read the repository commit" >&2; exit 1; }
|
||||
|
||||
DEST="$DLSITE/dl/$TOKEN"
|
||||
OUT="$DEST/payload-inputs.zip"
|
||||
rm -f "$OUT"
|
||||
(cd "$(dirname "$STAGE")" && zip -qr "$OUT" "payload-inputs" -x '*.DS_Store')
|
||||
# shellcheck source=packaging/windows/inputs-manifest.sh
|
||||
. "$HERE/inputs-manifest.sh"
|
||||
write_inputs_manifest "$STAGE" "$OUT" "$NODE_COMMIT" "$NODE_BRANCH" "$REPO_COMMIT" "$DEST/payload-inputs.json"
|
||||
"$SIGNER" sign-inputs "$KEY" "$DEST/payload-inputs.json" > "$DEST/payload-inputs.json.sig"
|
||||
# what the runner will do, done here first: the signature, the zip and the folder against the manifest
|
||||
"$SIGNER" verify-inputs "$PUB" "$DEST/payload-inputs.json" "$DEST/payload-inputs.json.sig" --zip "$OUT" --dir "$STAGE" --node-commit "$NODE_COMMIT"
|
||||
shasum -a 256 "$OUT" | awk '{print $1}' > "$DEST/payload-inputs.sha256"
|
||||
cp "$STAGE/inputs.json" "$DEST/payload-inputs.json"
|
||||
printf '%s\n' "$NODE_COMMIT" > "$HERE/node-source.pin"
|
||||
echo "payload-inputs.zip: $(stat -f %z "$OUT") bytes, sha256 $(cat "$DEST/payload-inputs.sha256")"
|
||||
cat "$DEST/payload-inputs.json"
|
||||
echo "signature: $(cut -c1-16 "$DEST/payload-inputs.json.sig")... (payload-inputs.json.sig)"
|
||||
echo "pinned node commit $NODE_COMMIT ($NODE_BRANCH) in packaging/windows/node-source.pin: commit it with this push, or the workflow refuses the manifest"
|
||||
rm -rf "$(dirname "$STAGE")"
|
||||
|
||||
if [ "$DEPLOY" = 1 ]; then
|
||||
|
|
|
|||
85
packaging/windows/test-inputs-signing.sh
Executable file
85
packaging/windows/test-inputs-signing.sh
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env bash
|
||||
# Mac-side test of the signed payload-inputs chain (review round 4, R4.5.2, ledger G13), run before any push-inputs:
|
||||
#
|
||||
# packaging/windows/test-inputs-signing.sh
|
||||
#
|
||||
# What it proves, with a throwaway key made for the run: the manifest writer (inputs-manifest.sh) produces what the
|
||||
# signer signs and the verifier accepts; the verifier then REFUSES a changed zip byte, a changed manifest byte, a
|
||||
# changed unpacked file, an unlisted file in the folder, a missing file, a wrong pinned commit, a signature by another
|
||||
# key, and the app's embedded key refuses the throwaway key. If ~/.config/igneum/ota-signing-key exists it also signs
|
||||
# the test manifest with the real key and verifies it with `embedded`, which proves the key the Mac signs with is the
|
||||
# key the runner trusts. Nothing is uploaded, deployed or written outside a temporary folder.
|
||||
#
|
||||
# A check is trusted only once it has been seen to fire on a known-good and a known-bad case (standing rule, 4 October
|
||||
# 2026), so every negative case here must FAIL for the run to pass.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
SIGNER="${IGNEUM_OTA_SIGN:-$ROOT/app/igneum-app/target/release/igneum-ota-sign}"
|
||||
[ -x "$SIGNER" ] || { echo "no $SIGNER: build it first (cd app/igneum-app && nice -n 19 cargo build --release -j 4 --bin igneum-ota-sign)" >&2; exit 1; }
|
||||
# shellcheck source=packaging/windows/inputs-manifest.sh
|
||||
. "$HERE/inputs-manifest.sh"
|
||||
|
||||
T="$(mktemp -d)"
|
||||
trap 'rm -rf "$T"' EXIT
|
||||
umask 077
|
||||
pass=0; fail=0
|
||||
ok() { pass=$((pass + 1)); echo " ok $1"; }
|
||||
bad() { fail=$((fail + 1)); echo " FAIL $1"; }
|
||||
expect_ok() { local what="$1"; shift; if "$@" > "$T/out" 2>&1; then ok "$what"; else bad "$what: $(tail -1 "$T/out")"; fi; }
|
||||
expect_fail() { local what="$1"; shift; if "$@" > "$T/out" 2>&1; then bad "$what: accepted, must refuse"; else ok "$what: refused ($(tail -1 "$T/out" | cut -c1-110))"; fi; }
|
||||
|
||||
# a stage folder shaped like push-inputs.sh's, a zip of it, the manifest, a throwaway key
|
||||
mkdir -p "$T/payload-inputs"
|
||||
printf 'not a real node\n' > "$T/payload-inputs/igneumd.exe"
|
||||
printf 'not a real miner\n' > "$T/payload-inputs/igneum-miner.exe"
|
||||
printf 'not a real worker\n' > "$T/payload-inputs/igneum-worker-cuda.exe"
|
||||
printf 'dll\n' > "$T/payload-inputs/nvrtc64_120_0.dll"
|
||||
(cd "$T" && zip -qr payload-inputs.zip payload-inputs)
|
||||
NODE="6aa69a45364b9b30a32695e33eb66f100c9be85f"
|
||||
REPO_C="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo 0000000000000000000000000000000000000000)"
|
||||
write_inputs_manifest "$T/payload-inputs" "$T/payload-inputs.zip" "$NODE" "finality-fixes" "$REPO_C" "$T/payload-inputs.json"
|
||||
"$SIGNER" keygen "$T/key" "$T/key.pub" > /dev/null
|
||||
"$SIGNER" keygen "$T/other" "$T/other.pub" > /dev/null
|
||||
printf '%s\n' "$NODE" > "$T/node-source.pin"
|
||||
|
||||
echo "sign and verify (throwaway key)"
|
||||
expect_ok "sign-inputs writes a 128-hex signature" bash -c "\"$SIGNER\" sign-inputs \"$T/key\" \"$T/payload-inputs.json\" > \"$T/payload-inputs.json.sig\" && [ \"\$(tr -d '[:space:]' < \"$T/payload-inputs.json.sig\" | wc -c | tr -d ' ')\" = 128 ]"
|
||||
expect_ok "verify-inputs: signature, zip and pin" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --zip "$T/payload-inputs.zip" --node-commit "$T/node-source.pin"
|
||||
expect_ok "verify-inputs: the unpacked folder" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --dir "$T/payload-inputs"
|
||||
expect_ok "verify-inputs: the pin as a literal" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --node-commit "$NODE"
|
||||
|
||||
echo "what must be refused"
|
||||
cp "$T/payload-inputs.zip" "$T/zip.bak"; printf 'x' >> "$T/payload-inputs.zip"
|
||||
expect_fail "one byte appended to the zip" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --zip "$T/payload-inputs.zip"
|
||||
cp "$T/zip.bak" "$T/payload-inputs.zip"
|
||||
sed 's/finality-fixes/finality-fixed/' "$T/payload-inputs.json" > "$T/tampered.json"
|
||||
expect_fail "one byte changed in the manifest" "$SIGNER" verify-inputs "$T/key.pub" "$T/tampered.json" "$T/payload-inputs.json.sig"
|
||||
printf 'tampered\n' > "$T/payload-inputs/igneumd.exe"
|
||||
expect_fail "a changed unpacked file" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --dir "$T/payload-inputs"
|
||||
printf 'not a real node\n' > "$T/payload-inputs/igneumd.exe"
|
||||
printf 'extra\n' > "$T/payload-inputs/extra.dll"
|
||||
expect_fail "an unlisted file in the folder" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --dir "$T/payload-inputs"
|
||||
rm "$T/payload-inputs/extra.dll"
|
||||
mv "$T/payload-inputs/nvrtc64_120_0.dll" "$T/dll.bak"
|
||||
expect_fail "a missing file" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --dir "$T/payload-inputs"
|
||||
mv "$T/dll.bak" "$T/payload-inputs/nvrtc64_120_0.dll"
|
||||
expect_fail "a wrong pinned commit" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --node-commit "${NODE/6aa6/7aa6}"
|
||||
expect_fail "a short pin" "$SIGNER" verify-inputs "$T/key.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig" --node-commit "6aa69a45"
|
||||
expect_fail "a signature by another key" "$SIGNER" verify-inputs "$T/other.pub" "$T/payload-inputs.json" "$T/payload-inputs.json.sig"
|
||||
expect_fail "the app's embedded key against the throwaway signature" "$SIGNER" verify-inputs embedded "$T/payload-inputs.json" "$T/payload-inputs.json.sig"
|
||||
sed 's/"igneum-miner.exe"/"igneum-miner.exe.bak"/' "$T/payload-inputs.json" > "$T/nominer.json"
|
||||
expect_fail "sign-inputs refuses a manifest without igneum-miner.exe" "$SIGNER" sign-inputs "$T/key" "$T/nominer.json"
|
||||
printf '{"format":"igneum-payload-inputs/1"}\n' > "$T/short.json"
|
||||
expect_fail "sign-inputs refuses a truncated manifest" "$SIGNER" sign-inputs "$T/key" "$T/short.json"
|
||||
|
||||
KEY="$HOME/.config/igneum/ota-signing-key"
|
||||
if [ -f "$KEY" ]; then
|
||||
echo "the real key (nothing leaves this folder)"
|
||||
expect_ok "sign with the Mac's OTA key, verify with the key compiled into the app" bash -c "\"$SIGNER\" sign-inputs \"$KEY\" \"$T/payload-inputs.json\" > \"$T/real.sig\" && \"$SIGNER\" verify-inputs embedded \"$T/payload-inputs.json\" \"$T/real.sig\" --zip \"$T/payload-inputs.zip\" --dir \"$T/payload-inputs\" --node-commit \"$T/node-source.pin\""
|
||||
else
|
||||
echo " skip the real-key case: no $KEY on this machine"
|
||||
fi
|
||||
|
||||
echo "$pass passed, $fail failed"
|
||||
[ "$fail" = 0 ]
|
||||
|
|
@ -163,6 +163,7 @@ p{margin:0;color:var(--ink-2);max-width:52ch}
|
|||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/wallet">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/#mine">Get the miner</a>
|
||||
</nav>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -244,7 +244,7 @@ if (existsSync(join(docs, 'bench-log.md'))) {
|
|||
// The hand-written pages: shared head, nav (with the active link marked) and footer injected in place; the homepage also
|
||||
// gets journey.json inlined so the phases and the log render without a fetch and without a layout shift.
|
||||
const journey = JSON.parse(readFileSync(join(here, 'journey.json'), 'utf8'));
|
||||
const PAGES = [['index.html', ''], ['litepaper.html', 'litepaper'], ['live.html', 'live'], ['evidence.html', 'evidence'], ['404.html', '']];
|
||||
const PAGES = [['index.html', ''], ['litepaper.html', 'litepaper'], ['live.html', 'live'], ['evidence.html', 'evidence'], ['wallet.html', ''], ['404.html', '']];
|
||||
for (const [file, active] of PAGES) {
|
||||
const p = join(here, file);
|
||||
if (!existsSync(p)) throw new Error(`missing page ${file}`);
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ code{font-family:var(--f-mono);font-size:.92em;background:var(--obsidian);paddin
|
|||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/wallet">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/#mine">Get the miner</a>
|
||||
</nav>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -50,6 +50,16 @@
|
|||
}
|
||||
],
|
||||
"log": [
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": ", ledger M30: the block and transaction floods grew the node by 256 MiB per epoch roll, fixed by sharing the PoW cache across the epochs of a day",
|
||||
"short": ", ledger M30: the block and transaction floods grew the node by 256…"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": ", round-4 consensus items F23, F24, G12, X18 and M31: unit tests and fast-time 3-node runs against the control",
|
||||
"short": ", round-4 consensus items F23, F24, G12, X18 and M31"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": ", the software dev fee measured on a test network: 9 fee blocks in 785, the chain and the miners' counters agree",
|
||||
|
|
@ -200,36 +210,16 @@
|
|||
"text": "Devnet-v4 integration: nine branches merged, 3-node test network on the merged node, Windows cross-build",
|
||||
"short": "Devnet v4: nine branches merged into one node"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Windows node package: igneumd cross-compiled for x86_64-pc-windows-gnu, two-peer sync test",
|
||||
"short": "Windows node package: cross-compiled, two-peer sync"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Igneum-node devnet v2: sustained-mining finality rule v2 on a four-miner test network, and as a follower of the live devnet",
|
||||
"short": "Finality rule v2 live on a four-miner test network"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Execution layer devnet v3: revm over the selected chain, 3-node simnet, viem smoke test",
|
||||
"short": "EVM execution layer: identical state on three nodes"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Weak-program census: 400,000 program runs through the CPU reference, the redundant-load finding, and the rules for M5 and M6",
|
||||
"short": "Census of 400,000 programs: redundant loads found"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Proving v0: first SP1 proof of an Igneum block, Apple M5 Max CPU, loaded machine",
|
||||
"short": "First SP1 proof of an Igneum block, on a laptop CPU"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Proto-opencl: OpenCL path built and proven without AMD silicon",
|
||||
"short": "OpenCL worker built and proven without AMD silicon"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "AMD gfx1036 , AMD OpenCL 2.1 driver 3652.0",
|
||||
"short": "AMD integrated GPU runs the hash through OpenCL"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "First devnet blocks on the real lottery hash: CPU, then Metal GPU, three worker implementations",
|
||||
|
|
@ -247,8 +237,18 @@
|
|||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "AMD gfx1036 , AMD OpenCL 2.1 driver 3652.0",
|
||||
"short": "AMD integrated GPU runs the hash through OpenCL"
|
||||
"text": "Weak-program census: 400,000 program runs through the CPU reference, the redundant-load finding, and the rules for M5 and M6",
|
||||
"short": "Census of 400,000 programs: redundant loads found"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "Proving v0: first SP1 proof of an Igneum block, Apple M5 Max CPU, loaded machine",
|
||||
"short": "First SP1 proof of an Igneum block, on a laptop CPU"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-03",
|
||||
"text": "RTX 5090 first run",
|
||||
"short": "RTX 5090 first run"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -526,7 +526,7 @@ body.all .pager{display:none}
|
|||
<p>The card hashes the lottery continuously. When the client sees a shard or an external job it can win, it switches the card to proving for a few seconds, posts the proof, and goes back to hashing. The client does the switching and the miner sees one balance.</p>
|
||||
<p>The protocol carries no fee: no dev fund, no cut to any team. The Igneum Miner software takes an optional 1% dev fee, the way other GPU miners do. One block template in 100 is requested with the dev address instead of yours, by a counter, not a random draw, so it is exactly 1 in 100 and anyone can check it from the source or from the chain. One flag turns it off (<code>--dev-fee 0</code>, a switch in the app, a line in the HiveOS config). The miner prints the fee and the address when it starts. Any other client is welcome.</p>
|
||||
<h3>One click, for everyone else</h3>
|
||||
<p>Farm operators get HiveOS support on day one. Everyone else gets the Igneum app: install it on Windows, macOS or Linux, press one button, and the card is mining and proving to a wallet the app made for you, with earnings shown in IGN and in your currency, and mining paused while you game. It is the same client with a face on it. The app shows you the seed phrase and has you confirm it before mining starts, offers a hardware wallet for your earnings, updates only when you accept a release signed by the key published in the genesis block, and is downloaded only from this domain or the repository with its hash shown beside the button. Nobody from Igneum will ever ask for your seed. Mining never runs in a browser, because browser compute is slow and browser mining has meant malware since Coinhive. The browser is for the dashboard, and for verifying the chain.</p>
|
||||
<p>Farm operators get HiveOS support on day one. Everyone else gets the Igneum app: install it on Windows, macOS or Linux, press one button, and the card is mining and proving to a wallet the app made for you, with earnings shown in IGN and in your currency, and mining paused while you game. It is the same client with a face on it. The app shows you the seed phrase and has you confirm it before mining starts. The address it makes is an ordinary Ethereum account: MetaMask holds it today (<a href="/wallet">chain id, RPC and the one-click button</a>) and the Igneum Wallet, with the Apps tab and the explorer built in, follows. The app updates only when you accept a release signed by the key published in the genesis block, and is downloaded only from this domain or the repository with its hash shown beside the button. Nobody from Igneum will ever ask for your seed. Mining never runs in a browser, because browser compute is slow and browser mining has meant malware since Coinhive. The browser is for the dashboard, and for verifying the chain.</p>
|
||||
<h3>Fair launch, announced</h3>
|
||||
<p>Launch date and miner software published a month ahead. Pools live on testnet. HiveOS support on day one. The founders mine from genesis like everyone else, with disclosed addresses and the same software. Nobody has coins before block one.</p>
|
||||
</section>
|
||||
|
|
@ -644,6 +644,7 @@ body.all .pager{display:none}
|
|||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/wallet">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/#mine">Get the miner</a>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@ main{padding-bottom:var(--sec)}
|
|||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/wallet">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/#mine">Get the miner</a>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:
|
|||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/wallet">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/#mine">Get the miner</a>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/wallet">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/#mine">Get the miner</a>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@
|
|||
<url><loc>https://igneum.network/bench</loc><lastmod>2026-10-04</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
|
||||
<url><loc>https://igneum.network/evidence</loc><lastmod>2026-10-04</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
|
||||
<url><loc>https://igneum.network/miners</loc><lastmod>2026-10-04</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
|
||||
<url><loc>https://igneum.network/wallet</loc><lastmod>2026-10-05</lastmod><changefreq>monthly</changefreq><priority>0.6</priority></url>
|
||||
</urlset>
|
||||
|
|
|
|||
287
site/wallet.html
Normal file
287
site/wallet.html
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Add Igneum to MetaMask</title>
|
||||
<meta name="description" content="Add the Igneum network to MetaMask or any Ethereum wallet in one click: chain id, RPC URL, the IGN symbol and 18 decimals. Devnet today, public testnet next.">
|
||||
<link rel="canonical" href="https://igneum.network/wallet">
|
||||
<meta name="theme-color" content="#0C0C0E">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Igneum">
|
||||
<meta property="og:title" content="Add Igneum to MetaMask">
|
||||
<meta property="og:description" content="Add the Igneum network to MetaMask or any Ethereum wallet in one click: chain id, RPC URL, the IGN symbol and 18 decimals. Devnet today, public testnet next.">
|
||||
<meta property="og:url" content="https://igneum.network/wallet">
|
||||
<meta property="og:image" content="https://igneum.network/og.png?v=3">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Add Igneum to MetaMask">
|
||||
<meta name="twitter:description" content="Add the Igneum network to MetaMask or any Ethereum wallet in one click: chain id, RPC URL, the IGN symbol and 18 decimals. Devnet today, public testnet next.">
|
||||
<meta name="twitter:image" content="https://igneum.network/og.png?v=3">
|
||||
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
|
||||
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
|
||||
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<!-- head:start -->
|
||||
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<style>
|
||||
/* Shared by every page. Source: site/partials/head.html, injected by site/build.mjs between the head markers. Edit the partial, not the page. */
|
||||
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. */
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
|
||||
:root{--f-sans:'IBM Plex Sans','Plex Sans Fallback',system-ui,-apple-system,sans-serif;--f-display:'Unbounded','Unbounded Fallback',sans-serif;--f-mono:'IBM Plex Mono','Plex Mono Fallback',ui-monospace,Menlo,monospace;
|
||||
/* site chrome tokens (nav, footer, skip link). Dark by default; the litepaper sets light values and resets these in dark mode */
|
||||
--nav-h:68px;--ui-bg:rgba(12,12,14,.84);--ui-menu:#0C0C0E;--ui-ink:#F4F1EC;--ui-ink-2:#C9C7C2;--ui-ash:#9A9A9E;--ui-line:#2A2A30;--ui-line-2:#3A3A42;--ui-accent:#F2541B;--ui-accent-ink:#0C0C0E;--ui-hot:#FFB35C;--ui-hover:#FF6A2B;--ui-tint:rgba(242,84,27,.12)}
|
||||
html{-webkit-text-size-adjust:100%}
|
||||
h1,h2,h3{text-wrap:balance}
|
||||
p,li,dd,figcaption{text-wrap:pretty}
|
||||
a:focus-visible,button:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--ui-accent);outline-offset:3px;border-radius:6px}
|
||||
.skip{position:absolute;left:12px;top:-80px;z-index:50;background:var(--ui-accent);color:var(--ui-accent-ink);padding:10px 14px;border-radius:10px;font:600 15px/1.2 var(--f-sans);text-decoration:none}
|
||||
.skip:focus{top:12px}
|
||||
/* nav: one bar on every page, the mark in its black square, five links and the miner button, a menu under 901 px */
|
||||
.nav{position:sticky;top:0;z-index:20;background:var(--ui-bg);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--ui-line);color:var(--ui-ink)}
|
||||
.nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:var(--nav-h)}
|
||||
.nav a{color:inherit;text-decoration:none}
|
||||
.brand{display:inline-flex;align-items:center;gap:10px;color:var(--ui-ink);text-decoration:none;flex:0 0 auto}
|
||||
.brand .mark{display:block;flex:0 0 auto}
|
||||
.brand .word{font-family:var(--f-display);font-weight:900;font-size:20px;letter-spacing:.06em;line-height:1}
|
||||
.nav .links{display:flex;align-items:center;gap:clamp(14px,2vw,26px);font-size:15px;font-weight:500}
|
||||
.nav .links a{color:var(--ui-ink-2);padding:4px 0;border-bottom:2px solid transparent;transition:color .15s ease,border-color .15s ease}
|
||||
.nav .links a:hover,.nav .brand:hover{color:var(--ui-hot);text-decoration:none}
|
||||
.nav .brand:hover{color:var(--ui-ink)}
|
||||
.nav .links a[aria-current="page"]{color:var(--ui-ink);border-bottom-color:var(--ui-accent)}
|
||||
.nav .links a.cta{display:inline-flex;align-items:center;justify-content:center;min-height:40px;padding:8px 16px;border-radius:10px;font-weight:600;background:var(--ui-accent);color:var(--ui-accent-ink);border:1px solid var(--ui-accent);transition:background .15s ease,transform .15s ease}
|
||||
.nav .links a.cta:hover{background:var(--ui-hover);border-color:var(--ui-hover);color:var(--ui-accent-ink);transform:translateY(-1px)}
|
||||
.burger{display:none;background:none;border:1px solid var(--ui-line-2);color:var(--ui-ink);border-radius:10px;width:44px;height:44px;align-items:center;justify-content:center;cursor:pointer;padding:0;flex:0 0 auto}
|
||||
.burger .x{display:none}.burger[aria-expanded="true"] .x{display:block}.burger[aria-expanded="true"] .bars{display:none}
|
||||
@media (max-width:900px){
|
||||
.nav .links{display:none;position:absolute;left:0;right:0;top:100%;background:var(--ui-menu);border-bottom:1px solid var(--ui-line);box-shadow:0 24px 40px rgba(0,0,0,.35);padding:10px var(--gutter,16px) 18px;flex-direction:column;align-items:stretch;gap:2px}
|
||||
.nav .links a{padding:13px 10px;border-radius:10px;border-bottom:0;font-size:16px}
|
||||
.nav .links a[aria-current="page"]{background:var(--ui-tint)}
|
||||
.nav .links a.cta{margin-top:8px;min-height:48px}
|
||||
.nav .links.open{display:flex}
|
||||
.burger{display:inline-flex}
|
||||
}
|
||||
/* footer, the same on every page */
|
||||
.foot{border-top:1px solid var(--ui-line);color:var(--ui-ink);margin-top:var(--sec,64px)}
|
||||
.foot .wrap{padding-block:48px 32px}
|
||||
.foot-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,200px),1fr));gap:32px}
|
||||
.foot-brand{display:flex;flex-direction:column;gap:12px;max-width:34ch}
|
||||
.foot-brand .brand{align-self:flex-start}.foot-brand .word{font-size:16px}
|
||||
.foot-brand p{color:var(--ui-ash);font-size:14px;margin:0}
|
||||
.foot-col{display:flex;flex-direction:column;gap:10px;font-size:15px}
|
||||
.foot-col .eyebrow{font-family:var(--f-mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ui-ash);margin-bottom:4px}
|
||||
.foot-col a{color:var(--ui-ink-2);text-decoration:none;display:inline-flex;align-items:center;gap:6px;align-self:flex-start}
|
||||
.foot-col a:hover{color:var(--ui-hot);text-decoration:none}
|
||||
.foot-base{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 24px;margin-top:36px;padding-top:20px;border-top:1px solid var(--ui-line);font-size:13px;color:var(--ui-ash)}
|
||||
.foot-base span{text-wrap:balance}.foot-base .mono{font-family:var(--f-mono)}
|
||||
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}
|
||||
</style>
|
||||
<!-- head:end -->
|
||||
<style>
|
||||
:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;
|
||||
--max:1200px;--gutter:clamp(16px,4vw,32px);--sec:clamp(56px,8vw,96px);--head:clamp(40px,6vw,64px);
|
||||
--card-pad:clamp(18px,3vw,28px);--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:24px;--gap-tile:12px;
|
||||
--fs-h1:clamp(32px,5.5vw,56px);--fs-h2:clamp(28px,4.2vw,44px);--fs-h3:clamp(18px,2vw,22px);--fs-tile:clamp(20px,2.2vw,26px)}
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--f-sans);font-size:16px;line-height:1.6;padding:0;-webkit-font-smoothing:antialiased}
|
||||
a{color:var(--ember);text-decoration:none}a:hover{text-decoration:underline}
|
||||
[id]{scroll-margin-top:calc(var(--nav-h) + 16px)}
|
||||
.wrap{max-width:var(--max);margin:0 auto;padding-inline:var(--gutter)}
|
||||
.eyebrow{font-family:var(--f-mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash)}
|
||||
.head{padding-top:var(--head)}
|
||||
h1{font-family:var(--f-display);font-weight:900;font-size:var(--fs-h1);line-height:1.05;margin:12px 0 12px}
|
||||
h2{font-family:var(--f-display);font-weight:700;font-size:var(--fs-h3);line-height:1.3;margin:44px 0 12px;padding-top:24px;border-top:1px solid var(--line)}
|
||||
.note{color:var(--ash);font-size:15px;max-width:72ch;margin:0 0 24px}
|
||||
.note b{color:var(--bone);font-weight:500}
|
||||
.grid{display:grid;gap:var(--gap);grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr));margin:8px 0 8px}
|
||||
.card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0}
|
||||
.card h3{font-family:var(--f-display);font-weight:700;font-size:var(--fs-h3);margin:0 0 6px}
|
||||
.card .tag{display:inline-block;font-family:var(--f-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--molten);border:1px solid var(--line);border-radius:999px;padding:3px 9px;margin-bottom:12px}
|
||||
.card p{margin:0 0 12px;color:var(--ink-2);font-size:15px}
|
||||
dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:6px 16px;margin:0 0 16px;font-size:14px}
|
||||
dt{font-family:var(--f-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ash);padding-top:3px}
|
||||
dd{margin:0;font-family:var(--f-mono);font-size:14px;color:var(--bone);overflow-wrap:anywhere}
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:48px;padding:12px 22px;border-radius:10px;font-weight:600;font-size:16px;border:1px solid var(--ember);color:var(--obsidian);background:var(--ember);cursor:pointer;font-family:inherit}
|
||||
.btn:hover{background:var(--molten);border-color:var(--molten);text-decoration:none}
|
||||
.btn[disabled]{opacity:.5;cursor:not-allowed}
|
||||
.btn.ghost{background:transparent;color:var(--bone);border-color:var(--line)}
|
||||
.status{font-family:var(--f-mono);font-size:13px;color:var(--ash);margin:10px 0 0;min-height:1.4em}
|
||||
ol{margin:0 0 14px;padding-left:22px}li{margin-bottom:6px}
|
||||
code{font-family:var(--f-mono);font-size:.92em;background:var(--graphite);padding:1px 5px;border-radius:4px;overflow-wrap:anywhere}
|
||||
pre{margin:0 0 16px;padding:16px 18px;background:var(--graphite);border-radius:var(--card-r);font-family:var(--f-mono);font-size:13px;line-height:1.55;overflow-x:auto}
|
||||
.asof{color:var(--ash);font-size:13px;margin:40px 0 0;max-width:72ch}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- nav:start -->
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<nav class="nav" aria-label="Main">
|
||||
<div class="wrap">
|
||||
<a href="/" class="brand" aria-label="Igneum home">
|
||||
<svg class="mark" viewBox="0 0 1024 1024" width="36" height="36" aria-hidden="true"><rect width="1024" height="1024" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
|
||||
<span class="word">IGNEUM</span>
|
||||
</a>
|
||||
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-links" aria-label="Menu"><svg class="bars" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"></path></svg><svg class="x" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"></path></svg></button>
|
||||
<div class="links" id="nav-links">
|
||||
<a href="/litepaper" data-nav="litepaper">Litepaper</a>
|
||||
<a href="/live" data-nav="live">Live devnet</a>
|
||||
<a href="/bench" data-nav="bench">Engineering log</a>
|
||||
<a href="/miners" data-nav="miners">Miners</a>
|
||||
<a href="/evidence" data-nav="evidence">Evidence</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener">GitHub</a>
|
||||
<a href="/#mine" class="cta">Get the miner</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<script>
|
||||
(function(){
|
||||
// the menu: open on the button, closed by any link, a tap outside, Escape (focus back on the button) or a resize past 900 px
|
||||
var b=document.getElementById('nav-burger'),l=document.getElementById('nav-links');if(!b||!l)return;
|
||||
function set(o){l.classList.toggle('open',o);b.setAttribute('aria-expanded',o?'true':'false');}
|
||||
b.addEventListener('click',function(){set(!l.classList.contains('open'));});
|
||||
l.addEventListener('click',function(e){if(e.target.closest('a'))set(false);});
|
||||
document.addEventListener('click',function(e){if(l.classList.contains('open')&&!l.contains(e.target)&&!b.contains(e.target))set(false);});
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&l.classList.contains('open')){set(false);b.focus();}});
|
||||
var mq=window.matchMedia('(min-width:901px)');if(mq.addEventListener)mq.addEventListener('change',function(){set(false);});
|
||||
})();
|
||||
</script>
|
||||
<!-- nav:end -->
|
||||
<main id="main" class="wrap">
|
||||
<div class="head">
|
||||
<div class="eyebrow">Wallets · chain id · RPC</div>
|
||||
<h1>Add Igneum to MetaMask</h1>
|
||||
</div>
|
||||
<p class="note">Igneum runs the Ethereum virtual machine, so MetaMask and every other Ethereum wallet work unchanged. The wallet needs four things: the chain id, an RPC URL, the symbol <b>IGN</b> and <b>18</b> decimals. The button below sends them to the wallet with the standard <code>wallet_addEthereumChain</code> request; the wallet shows them to you and asks before adding anything. Nothing on this page asks for a seed phrase or a key. Nobody from Igneum will ask for your seed.</p>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card" id="testnet">
|
||||
<span class="tag">Public testnet · coming</span>
|
||||
<h3>Igneum testnet</h3>
|
||||
<p>The network the one-click miner app joins at public testnet. Coins on it have no value and the chain resets with notice (<a href="/#testnet-terms">testnet terms</a>).</p>
|
||||
<dl>
|
||||
<dt>Network name</dt><dd>Igneum Testnet</dd>
|
||||
<dt>Chain id</dt><dd>4462 (0x116e)</dd>
|
||||
<dt>RPC URL</dt><dd id="testnet-rpc">https://rpc.testnet.igneum.network <span style="color:var(--ash)">(published with the testnet)</span></dd>
|
||||
<dt>Symbol</dt><dd>IGN</dd>
|
||||
<dt>Decimals</dt><dd>18</dd>
|
||||
<dt>Explorer</dt><dd>published with the testnet</dd>
|
||||
</dl>
|
||||
<button class="btn" type="button" data-add="testnet" disabled>Add Igneum Testnet to MetaMask</button>
|
||||
<p class="status" data-status="testnet">The button switches on when the public RPC is live.</p>
|
||||
</div>
|
||||
<div class="card" id="devnet">
|
||||
<span class="tag">Devnet · live now</span>
|
||||
<h3>Igneum devnet, through your own node</h3>
|
||||
<p>The Igneum Miner app runs a full node on your machine and serves the Ethereum RPC on it. Point the wallet at that node. The devnet is a developer network: it resets without notice and its coins have no value.</p>
|
||||
<dl>
|
||||
<dt>Network name</dt><dd>Igneum Devnet (local node)</dd>
|
||||
<dt>Chain id</dt><dd>4463 (0x116f)</dd>
|
||||
<dt>RPC URL</dt><dd>http://127.0.0.1:26790</dd>
|
||||
<dt>Symbol</dt><dd>IGN</dd>
|
||||
<dt>Decimals</dt><dd>18</dd>
|
||||
<dt>Explorer</dt><dd>none yet</dd>
|
||||
</dl>
|
||||
<button class="btn" type="button" data-add="devnet">Add Igneum Devnet to MetaMask</button>
|
||||
<p class="status" data-status="devnet"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id="manual">Add it by hand</h2>
|
||||
<p class="note">If the wallet has no one-click support, or you prefer to type: MetaMask, Settings, Networks, Add a network manually. Enter the values from the card above. Any wallet that supports custom EVM networks takes the same four fields.</p>
|
||||
<ol>
|
||||
<li>Network name: Igneum Testnet (or Igneum Devnet for your own node).</li>
|
||||
<li>RPC URL: the one on the card.</li>
|
||||
<li>Chain id: 4462 for the testnet, 4463 for the devnet.</li>
|
||||
<li>Currency symbol: IGN. Decimals: 18.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="builders">For builders</h2>
|
||||
<p class="note">The same request from your own page or app, so your users land on the right chain:</p>
|
||||
<pre>await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: '0x116e', // 4462, the Igneum testnet (0x116f = 4463, the devnet)
|
||||
chainName: 'Igneum Testnet',
|
||||
nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 },
|
||||
rpcUrls: ['https://rpc.testnet.igneum.network'],
|
||||
blockExplorerUrls: []
|
||||
}]
|
||||
});</pre>
|
||||
<p class="note">Igneum signs transactions with the Ethereum rules (EIP-155, EIP-1559 and legacy envelopes). A transaction signed for another chain id is refused. Gas has two dimensions on Igneum, execution and proving, and the node folds the second into the price it quotes, so <code>eth_gasPrice</code> and <code>eth_estimateGas</code> work as they do on Ethereum. The <a href="/litepaper#builders-ask">litepaper</a> has the differences.</p>
|
||||
|
||||
<h2 id="app-wallet">The app and the Igneum Wallet</h2>
|
||||
<p class="note">The Igneum Miner app makes an address for your earnings and shows you its seed phrase once. That address is an ordinary Ethereum account: import the seed into MetaMask and the balance is there. An Igneum Wallet with the Apps tab and the explorer built in is in the roadmap; until it ships, MetaMask is the wallet.</p>
|
||||
|
||||
<p class="asof">Chain ids 4461 (mainnet), 4462 (testnet) and 4463 (devnet) are fixed in the node. The testnet RPC URL above is a placeholder until the testnet opens; this page is updated the day it does.</p>
|
||||
</main>
|
||||
<!-- footer:start -->
|
||||
<footer class="foot">
|
||||
<div class="wrap">
|
||||
<div class="foot-grid">
|
||||
<div class="foot-brand">
|
||||
<a href="/" class="brand" aria-label="Igneum home"><svg class="mark" viewBox="0 0 1024 1024" width="30" height="30" aria-hidden="true"><rect width="1024" height="1024" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span></a>
|
||||
<p>Mined by GPUs. Proven by fire.</p>
|
||||
</div>
|
||||
<nav class="foot-col" aria-label="Read">
|
||||
<div class="eyebrow">Read</div>
|
||||
<a href="/litepaper">Litepaper</a>
|
||||
<a href="/litepaper#limits">What Igneum does not claim</a>
|
||||
<a href="/litepaper#randomx">Igneum vs RandomX</a>
|
||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||
<a href="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Follow">
|
||||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/wallet">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/#mine">Get the miner</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="foot-base">
|
||||
<span>© 2026 Igneum. Nothing on this page is an offer to sell anything.</span>
|
||||
<span class="mono">igneum.network</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- footer:end -->
|
||||
<script>
|
||||
(function(){
|
||||
// one request per button; the wallet shows the chain to the user and decides. No key, no seed, no signature here.
|
||||
var CHAINS = {
|
||||
testnet: { chainId: '0x116e', chainName: 'Igneum Testnet', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['https://rpc.testnet.igneum.network'], blockExplorerUrls: [] },
|
||||
devnet: { chainId: '0x116f', chainName: 'Igneum Devnet (local node)', nativeCurrency: { name: 'Igneum', symbol: 'IGN', decimals: 18 }, rpcUrls: ['http://127.0.0.1:26790'], blockExplorerUrls: [] }
|
||||
};
|
||||
function status(name, text) { var el = document.querySelector('[data-status="' + name + '"]'); if (el) el.textContent = text; }
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-add]'), function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var name = btn.getAttribute('data-add'); var chain = CHAINS[name];
|
||||
if (!window.ethereum || !window.ethereum.request) { status(name, 'No Ethereum wallet found in this browser. Install MetaMask, or add the network by hand (below).'); return; }
|
||||
status(name, 'Waiting for the wallet...');
|
||||
window.ethereum.request({ method: 'wallet_addEthereumChain', params: [chain] })
|
||||
.then(function () { status(name, 'Added. The wallet is on ' + chain.chainName + ' (chain id ' + parseInt(chain.chainId, 16) + ').'); })
|
||||
.catch(function (e) { status(name, e && e.code === 4001 ? 'Cancelled in the wallet.' : 'The wallet refused: ' + ((e && e.message) || 'unknown error') + '. Add it by hand (below).'); });
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
101
tools/ci/check-workflow-shell.mjs
Normal file
101
tools/ci/check-workflow-shell.mjs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Mac-side (and CI) parse check of the shell inside .github/workflows/*.yml, so a broken `run:` block is caught before
|
||||
// a Windows runner spends twenty minutes on it (4 October 2026, the signed-inputs step of windows.yml).
|
||||
//
|
||||
// node tools/ci/check-workflow-shell.mjs [workflow.yml ...] default: every workflow under .github/workflows
|
||||
//
|
||||
// For every step with a `run: |` block: `shell: bash` (or no shell on an ubuntu job) goes through `bash -n`;
|
||||
// `shell: powershell` and `shell: pwsh` blocks, and every .ps1 the Windows folders hold, are checked against the one
|
||||
// rule Windows PowerShell 5.1 enforces that newer parsers may not: a drive-qualified variable reference "$name: text"
|
||||
// inside a double-quoted string (tools/ci/windows/check-ps51.ps1 runs the real 5.1 parser on the runner; this is the
|
||||
// Mac approximation of its rule, with the same negative fixture). `shell: cmd` blocks are checked for the bare ")"
|
||||
// class only when they span more than one line. Exit 1 on any finding, with file:line.
|
||||
import { readFileSync, readdirSync, writeFileSync, mkdtempSync, rmSync, existsSync, statSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repo = join(here, '..', '..');
|
||||
const wfDir = join(repo, '.github', 'workflows');
|
||||
const files = process.argv.length > 2 ? process.argv.slice(2) : readdirSync(wfDir).filter(f => /\.ya?ml$/.test(f)).map(f => join(wfDir, f));
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'wf-shell-'));
|
||||
let findings = 0, blocks = 0, ps1 = 0;
|
||||
const say = (file, line, msg) => { findings++; console.log(`${file}:${line}: ${msg}`); };
|
||||
|
||||
// The 5.1 rule: inside a double-quoted string, `$identifier:` is read as a drive-qualified variable reference
|
||||
// (`$env:PATH`, `$script:node`), so when the character after the colon cannot start a variable name (a space, a
|
||||
// `$`, punctuation or the closing quote) 5.1 fails with "Variable reference is not valid. ':' was not followed by a
|
||||
// valid variable name character". `$env:PATH`, `$script:x`, `${name}:` and `$($name):` are fine.
|
||||
const DRIVE_REF = /"(?:[^"\\]|\\.|`")*?\$[A-Za-z_][A-Za-z0-9_]*:(?![A-Za-z0-9_])(?:[^"\\]|\\.|`")*"/;
|
||||
function checkPowerShell(text, file, firstLine) {
|
||||
const lines = text.split('\n');
|
||||
lines.forEach((l, i) => {
|
||||
const noComment = l.replace(/^\s*#.*$/, '');
|
||||
if (DRIVE_REF.test(noComment) && !/\$\{[A-Za-z_][A-Za-z0-9_]*\}:/.test(noComment)) say(file, firstLine + i, `PowerShell 5.1 rejects "$name: text" (drive-qualified variable reference): ${l.trim().slice(0, 100)}`);
|
||||
});
|
||||
}
|
||||
// the same negative fixture check-ps51.ps1 uses: the Mac rule must bite on it or it proves nothing
|
||||
const fixture = join(repo, 'tools', 'ci', 'windows', 'fixtures', 'bad-drive-ref.ps1.txt');
|
||||
if (existsSync(fixture)) {
|
||||
const before = findings;
|
||||
checkPowerShell(readFileSync(fixture, 'utf8'), 'fixture', 1);
|
||||
if (findings === before) { console.log('self-test failed: the Mac rule does not fire on tools/ci/windows/fixtures/bad-drive-ref.ps1.txt'); process.exit(2); }
|
||||
findings = before; console.log('self-test: the 5.1 drive-reference rule fires on the fixture');
|
||||
}
|
||||
|
||||
function checkBash(text, file, firstLine) {
|
||||
const p = join(tmp, `block-${blocks}.sh`);
|
||||
writeFileSync(p, text);
|
||||
const r = spawnSync('bash', ['-n', p], { encoding: 'utf8' });
|
||||
if (r.status !== 0) say(file, firstLine, `bash -n: ${(r.stderr || '').trim().replace(p, 'block').split('\n')[0]}`);
|
||||
}
|
||||
function checkCmd(text, file, firstLine) {
|
||||
text.split('\n').forEach((l, i) => { if (/^\s*\)\s*$/.test(l)) say(file, firstLine + i, `a bare ")" line in a cmd block (the 3 October class)`); });
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const rel = file.startsWith(repo) ? file.slice(repo.length + 1) : file;
|
||||
const lines = readFileSync(file, 'utf8').split('\n');
|
||||
let runsOn = '';
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const m = /^(\s*)runs-on:\s*(\S+)/.exec(lines[i]); if (m) runsOn = m[2];
|
||||
const r = /^(\s*)run:\s*\|\s*$/.exec(lines[i]);
|
||||
if (!r) continue;
|
||||
const indent = r[1].length;
|
||||
// the step's shell: look back to the step's "- name:" for a `shell:` key at the same indent as `run:`
|
||||
let shell = '';
|
||||
for (let k = i - 1; k >= 0; k--) {
|
||||
const s = /^(\s*)shell:\s*(\S+)/.exec(lines[k]);
|
||||
if (s && s[1].length === indent) { shell = s[2]; break; }
|
||||
if (/^\s*-\s+(name|uses|run):/.test(lines[k]) && /^\s*-/.test(lines[k]) && lines[k].search(/\S/) < indent) break;
|
||||
}
|
||||
// the block: every following line indented deeper than `run:`
|
||||
const body = [];
|
||||
let j = i + 1;
|
||||
while (j < lines.length && (lines[j].trim() === '' || lines[j].search(/\S/) > indent)) { body.push(lines[j]); j++; }
|
||||
while (body.length && body[body.length - 1].trim() === '') body.pop();
|
||||
const bodyIndent = Math.min(...body.filter(l => l.trim()).map(l => l.search(/\S/)));
|
||||
const text = body.map(l => l.slice(bodyIndent)).join('\n') + '\n';
|
||||
const firstLine = i + 2;
|
||||
blocks++;
|
||||
const kind = shell || (runsOn.startsWith('windows') ? 'pwsh' : 'bash');
|
||||
if (kind === 'bash') checkBash(text, rel, firstLine);
|
||||
else if (kind === 'powershell' || kind === 'pwsh') checkPowerShell(text, rel, firstLine);
|
||||
else if (kind === 'cmd') checkCmd(text, rel, firstLine);
|
||||
i = j - 1;
|
||||
}
|
||||
}
|
||||
// every .ps1 the Windows folders hold, the same rule
|
||||
const folders = ['proto-cuda/windows-app', 'proto-cuda/windows-miner', 'proto-cuda/windows-node', 'proving/windows-wsl2', 'relay/clients', 'relay/playbooks', 'packaging/windows', 'app/windows', 'tools/ci/windows'];
|
||||
function walk(d) { if (!existsSync(d)) return []; return readdirSync(d).flatMap(f => { const p = join(d, f); return statSync(p).isDirectory() ? walk(p) : (f.endsWith('.ps1') ? [p] : []); }); }
|
||||
for (const f of folders) for (const p of walk(join(repo, f))) { ps1++; checkPowerShell(readFileSync(p, 'utf8'), p.slice(repo.length + 1), 1); }
|
||||
// the shell scripts the workflow and the Mac side run
|
||||
for (const f of ['packaging/windows/push-inputs.sh', 'packaging/windows/fetch-ci-artifacts.sh', 'packaging/windows/inputs-manifest.sh', 'packaging/windows/test-inputs-signing.sh', 'packaging/ota/publish-manifest.sh', 'packaging/windows/make-payload.sh']) {
|
||||
const p = join(repo, f); if (!existsSync(p)) continue;
|
||||
const r = spawnSync('bash', ['-n', p], { encoding: 'utf8' });
|
||||
if (r.status !== 0) say(f, 1, `bash -n: ${(r.stderr || '').trim().split('\n')[0]}`);
|
||||
}
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
console.log(`workflow shell: ${blocks} run blocks in ${files.length} workflow(s), ${ps1} .ps1 files, ${findings} finding(s)`);
|
||||
process.exit(findings ? 1 : 0);
|
||||
Loading…
Reference in a new issue