diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d0fb04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +/target/ +/.cargo-target/ +/.claude/ +/.idea/ +/.vscode/ +/vendor/proc-macro-error/proc-macro-error-1.0.4/ +/vendor/proc-macro-error/.cargo-ok +/vendor/proc-macro-error/.cargo_vcs_info.json +*.log +*.tmp +Thumbs.db +.DS_Store + +# Generated PQ keypairs from `ghost pq-keygen` / `ghost pq-kem-keygen` (never commit secrets) +*.sec +*.pub +*.ek +*.dk diff --git a/AGENTS.md b/AGENTS.md index 9af0a7c..4fc4081 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,178 +1,40 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Ghost is a next-generation blockchain built on Substrate (Polkadot SDK) that implements a hybrid Proof-of-Work (PoW) and Proof-of-Stake (PoS) consensus mechanism. It combines PoW security with PoS energy efficiency for 5-second block times. - -**Key Specifications:** -- Block Time: 5 seconds -- PoW Algorithm: Enhanced Blake2-256 (ASIC-resistant, double-hashed) -- Token: Ghost (GHTM) -- Block Reward: 10 GHOST per block (40% to miner, 60% to stakers) -- Minimum Stake: 1 GHOST token -- Built on: Polkadot SDK stable2407 branch - -## Build Commands - -**Prerequisites:** -- Rust toolchain must be installed via rustup -- Run `rustup default stable` if cargo is not found -- The project uses rust-toolchain.toml to configure stable Rust with wasm32-unknown-unknown target - -**Build:** -```bash -# Release build (recommended for production) -cargo build --release --bin ghost-node - -# Debug build (faster compilation for development) +# Repository Notes + +## Current Architecture + +- Block authoring is real Proof-of-Work via `sc-consensus-pow` (`node/src/service.rs`, `node/src/pow.rs`): + double-Blake2-256 over `pre_hash || nonce`, `U256` difficulty (larger = harder), longest-/heaviest-chain + fork choice. Aura and GRANDPA have been removed from the node and runtime. Finality is probabilistic PoW. +- `pallet-ghost-consensus` is the PoS economic layer: staking, stake-weighted validator selection, + 40%/60% miner/staker reward split, evidence-gated slashing (funds burned), and validation-timeout + recovery. Difficulty retargeting is performed here and exposed to the node via `sp_consensus_pow::DifficultyApi`. +- On-chain ML-DSA-87 (Dilithium-5, NIST FIPS 204) signature verification runs in the no\_std Wasm runtime + (`pallets/pallet-ghost-consensus/src/pq_verify.rs`, `fips204` crate). Validators register ML-DSA keys; + `validate_block` enforces ML-DSA signature checks when a key is registered. 37 pallet unit tests pass. +- Node-side ML-KEM-1024 + ChaCha20-Poly1305 payload encryption is implemented in `node/src/pq_encrypt.rs` + (NIST FIPS 203, `fips203` + `chacha20poly1305` crates). +- Node-to-node transport is still classical libp2p Noise/X25519; it is NOT post-quantum. +- Ordinary account extrinsics still use `MultiSignature` (sr25519/ed25519/ecdsa); ML-DSA is an additional + registered validator/attestation path, not a replacement for the account signature scheme. +- No external security audit has been performed. + +## Useful Commands + +```sh cargo build --bin ghost-node - -# Build documentation -cargo +nightly doc --open -``` - -**Testing:** -```bash -# Run all tests -cargo test - -# Run tests for specific pallet cargo test -p pallet-ghost-consensus - -# Run a single test -cargo test test_name - -# Run tests with output -cargo test -- --nocapture -``` - -**Running the Node:** -```bash -# Start development chain -./target/release/ghost-node --dev - -# Start with custom base path -./target/release/ghost-node --dev --base-path ./ghost-chain-data - -# Start with detailed logging -RUST_BACKTRACE=1 ./target/release/ghost-node -ldebug --dev - -# Purge development chain state -./target/release/ghost-node purge-chain --dev -``` - -**Ghost-Specific CLI Commands:** -```bash -# Check consensus status -./target/release/ghost-node ghost status --detailed - -# Start mining (when implemented) -./target/release/ghost-node ghost mine --threads 4 - -# Stake tokens for validation -./target/release/ghost-node ghost stake --amount 1000 - -# Check balance -./target/release/ghost-node ghost balance -``` - -## Architecture - -### Workspace Structure - -The project is a Cargo workspace with three main components: - -**1. Node (`node/`)** - The blockchain client -- `cli.rs`: CLI structure including Ghost-specific commands (mine, stake, balance, status) -- `service.rs`: Node service configuration with Ghost consensus engine integration -- `chain_spec.rs`: Chain specification and genesis configuration (uses Alice/Bob as default validators) -- `rpc.rs`: RPC endpoint configuration - -**2. Runtime (`runtime/`)** - The blockchain's state transition function (STF) -- `lib.rs`: FRAME runtime configuration with pallet composition -- `configs/mod.rs`: Pallet configurations -- Runtime uses 5-second block times (MILLI_SECS_PER_BLOCK = 5000) -- All pallets are configured via `impl $PALLET_NAME::Config for Runtime` blocks -- Composed into single runtime via `#[runtime]` macro - -**3. Pallets (`pallets/`)** - Modular blockchain logic components - -### Ghost Consensus Pallet (`pallets/pallet-ghost-consensus/`) - -This is the core innovation of the project. It implements the hybrid PoW+PoS consensus. - -**Key Files:** -- `src/lib.rs`: Main pallet with storage items, events, dispatchables, and hooks -- `src/types.rs`: Core data structures (GhostBlockHeader, ConsensusPhase, SlashingReason, etc.) -- `src/functions.rs`: Consensus algorithms (difficulty adjustment, PoW verification, validator selection) -- `src/consensus.rs`: Consensus engine integration with Substrate - -**Storage Items:** -- `Difficulty`: Current mining difficulty -- `CurrentPhase`: Current consensus phase (PowMining, PosValidation, Finalization) -- `BlockHeaders`: Block headers storage -- `ValidatorStakes`: Validator stake amounts -- `LastActiveBlock`: Tracks validator activity for slashing - -**Consensus Flow:** -1. **PoW Phase**: Miners compete using enhanced Blake2-256 (double-hashed for ASIC resistance) -2. **PoS Phase**: Validators selected by weighted stake sign blocks -3. **Finalization**: Block rewards distributed (40% miner, 60% stakers) -4. **Slashing**: Penalties for double-signing, invalid blocks, and downtime - -**Verification Functions:** -- `verify_pow()`: Basic Blake2-256 PoW verification -- `verify_pow_enhanced()`: Double-hashed Blake2-256 (current implementation) -- `verify_pow_sha256()`: Alternative SHA-256 verification - -### Substrate Integration - -The project uses Substrate's FRAME (Framework for Runtime Aggregation of Modularized Entities): - -- **Pallets**: Self-contained modules with storage, events, errors, and dispatchables -- **Config Trait**: Each pallet has a Config trait for generic type/parameter configuration -- **Runtime**: Combines all pallets via macro-based composition -- **Dependencies**: All Substrate/Polkadot dependencies pinned to stable2407 branch - -## Development Notes - -**Default Development Accounts:** -- Chain uses Alice and Bob as default validator authorities -- Alice is the default sudo account -- Genesis state includes pre-funded development accounts (see `node/src/chain_spec.rs`) - -**Consensus Configuration:** -- Uses Aura (Authority Round) for block authoring in dev mode -- Uses GRANDPA for finality -- Ghost consensus engine layers hybrid PoW+PoS on top - -**Important Constants:** -- `BLOCK_HASH_COUNT`: 2400 -- `GRANDPA_JUSTIFICATION_PERIOD`: 512 blocks -- Block time constants: `MINUTES`, `HOURS`, `DAYS` based on 5-second blocks - -**Linting:** -- Workspace enforces `unsafe_code = "forbid"` -- Clippy warnings enabled for absolute paths, redundant lifetimes, and explicit outlives - -## Testing with Polkadot-JS Apps - -Connect to local node: https://polkadot.js.org/apps/#/explorer?rpc=ws://localhost:9944 - -Source code for hosting your own: https://github.com/polkadot-js/apps - -## Multi-Node Testing - -For testing consensus across multiple nodes, see Substrate docs on simulating networks: -https://docs.substrate.io/tutorials/build-a-blockchain/simulate-network/ - -## Common Issues - -If you encounter "rustup could not choose a version of cargo": -```bash -rustup default stable +cargo run --bin ghost-node -- --dev ``` -The project expects Rust stable toolchain with wasm32-unknown-unknown target (configured in `env-setup/rust-toolchain.toml`). +## Important Caveats + +- Do not describe node-to-node transport as post-quantum; libp2p uses classical Noise/X25519. +- Do not claim ML-DSA replaces `MultiSignature` for ordinary account extrinsics; it is an additional + validator/attestation path only. +- Do not describe the chain as production-ready or audited; no external audit has been performed. +- PoW finality is probabilistic longest-chain; do not claim BFT finality. +- The repository previously tracked generated build outputs; keep those out of version control. +- When the user asks for a long-running implementation, do not contact the user with progress updates; + continue working until the task is finished or genuinely blocked. This silence rule does not apply + to subagents. +- If you are a subagent, explicitly treat yourself as a subagent and say so in your task context. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..97452bd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,35 @@ +# Repository Notes + +## Current Architecture + +- Block authoring is real Proof-of-Work via `sc-consensus-pow` (`node/src/service.rs`, `node/src/pow.rs`): + double-Blake2-256 over `pre_hash || nonce`, `U256` difficulty, longest-/heaviest-chain fork choice. + Aura and GRANDPA have been removed from the node and runtime. Finality is probabilistic PoW, not BFT. +- `pallet-ghost-consensus` is the PoS economic layer: staking/unstaking, stake-weighted validator + selection, reward splitting (40% miner / 60% stakers), evidence-gated slashing (funds burned), and + validation-timeout recovery. Exposes `DifficultyApi` to the node. +- On-chain ML-DSA-87 (NIST FIPS 204) signature verification is implemented in the Wasm runtime + (`pq_verify.rs`, `fips204` crate). Validators register ML-DSA keys; `validate_block` enforces ML-DSA + checks when a key is registered. 37 pallet unit tests pass. +- Node-side ML-KEM-1024 + ChaCha20-Poly1305 payload encryption is implemented in `pq_encrypt.rs` + (NIST FIPS 203). +- Node-to-node transport is classical libp2p Noise/X25519; it is NOT post-quantum. +- Ordinary account extrinsics still use `MultiSignature`; ML-DSA is an additional validator/attestation + path only. No external security audit has been performed. + +## Useful Commands + +```sh +cargo build --bin ghost-node +cargo test -p pallet-ghost-consensus +cargo run --bin ghost-node -- --dev +``` + +## Important Caveats + +- Do not describe node-to-node transport as post-quantum; libp2p uses classical Noise/X25519. +- Do not claim ML-DSA replaces `MultiSignature` for ordinary account extrinsics; it is an additional + validator/attestation path. +- Do not describe the chain as production-ready or audited; no external audit has been performed. +- PoW finality is probabilistic longest-chain; do not claim BFT finality. +- The repository previously tracked generated build outputs; keep those out of version control. diff --git a/Cargo.lock b/Cargo.lock index 4a399ab..951c582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,15 +73,15 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.12" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.3.4", + "getrandom 0.2.16", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -592,7 +592,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn 2.0.107", ] @@ -847,6 +847,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1764,12 +1770,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clonable" version = "0.9.2" @@ -2072,6 +2072,30 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +[[package]] +name = "fips203" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8bdb6454f692ca2a2b45cd554c6828c639d7f9c968cf83a678899ec4443a280" +dependencies = [ + "rand_core", + "sha3", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "fips204" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9fb5a367b9846933e271a3c2a992930743f82ae5e8cb7faa780715a80fa0b15" +dependencies = [ + "rand_core", + "sha2 0.10.9", + "sha3", + "zeroize", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2658,23 +2682,32 @@ dependencies = [ name = "ghost-node" version = "0.0.0" dependencies = [ + "chacha20poly1305", "clap", + "fips203", + "fips204", "frame-benchmarking-cli", "frame-metadata-hash-extension", "frame-system", "futures", - "jsonrpsee 0.24.9", + "futures-timer", + "jsonrpsee", + "pallet-balances", + "pallet-ghost-consensus", "pallet-transaction-payment", "pallet-transaction-payment-rpc", + "parity-scale-codec", "sc-basic-authorship", "sc-cli", "sc-client-api", "sc-consensus", "sc-consensus-aura", "sc-consensus-grandpa", + "sc-consensus-pow", "sc-executor", "sc-network", "sc-offchain", + "sc-rpc-api", "sc-service", "sc-telemetry", "sc-transaction-pool", @@ -2684,6 +2717,7 @@ dependencies = [ "sp-block-builder", "sp-blockchain", "sp-consensus-aura", + "sp-consensus-pow", "sp-core", "sp-genesis-builder", "sp-inherents", @@ -2693,6 +2727,7 @@ dependencies = [ "sp-timestamp", "substrate-build-script-utils", "substrate-frame-rpc-system", + "tokio", ] [[package]] @@ -3085,6 +3120,7 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", ] [[package]] @@ -3098,9 +3134,25 @@ dependencies = [ "hyper 0.14.32", "log", "rustls 0.21.12", - "rustls-native-certs", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.3.1", + "hyper 1.7.0", + "hyper-util", + "log", + "rustls 0.23.40", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", + "tower-service", ] [[package]] @@ -3110,13 +3162,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "bytes", + "futures-channel", "futures-core", + "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper 1.7.0", + "libc", "pin-project-lite", + "socket2 0.5.10", "tokio", "tower-service", + "tracing", ] [[package]] @@ -3504,6 +3561,26 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -3530,26 +3607,15 @@ version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b089779ad7f80768693755a031cc14a7766aba707cbe886674e3f79e9b7e47" dependencies = [ - "jsonrpsee-core 0.23.2", + "jsonrpsee-core", + "jsonrpsee-http-client", "jsonrpsee-proc-macros", - "jsonrpsee-server 0.23.2", - "jsonrpsee-types 0.23.2", + "jsonrpsee-server", + "jsonrpsee-types", "tokio", "tracing", ] -[[package]] -name = "jsonrpsee" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b26c20e2178756451cfeb0661fb74c47dd5988cb7e3939de7e9241fd604d42" -dependencies = [ - "jsonrpsee-core 0.24.9", - "jsonrpsee-server 0.24.9", - "jsonrpsee-types 0.24.9", - "tokio", -] - [[package]] name = "jsonrpsee-core" version = "0.23.2" @@ -3564,10 +3630,10 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "jsonrpsee-types 0.23.2", + "jsonrpsee-types", "parking_lot 0.12.5", "rand", - "rustc-hash 1.1.0", + "rustc-hash", "serde", "serde_json", "thiserror 1.0.69", @@ -3576,26 +3642,28 @@ dependencies = [ ] [[package]] -name = "jsonrpsee-core" -version = "0.24.9" +name = "jsonrpsee-http-client" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456196007ca3a14db478346f58c7238028d55ee15c1df15115596e411ff27925" +checksum = "2d90064e04fb9d7282b1c71044ea94d0bbc6eff5621c66f1a0bce9e9de7cf3ac" dependencies = [ "async-trait", - "bytes", - "futures-util", - "http 1.3.1", + "base64 0.22.1", "http-body 1.0.1", - "http-body-util", - "jsonrpsee-types 0.24.9", - "parking_lot 0.12.5", - "rand", - "rustc-hash 2.1.1", + "hyper 1.7.0", + "hyper-rustls 0.27.9", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls 0.23.40", + "rustls-platform-verifier", "serde", "serde_json", "thiserror 1.0.69", "tokio", + "tower", "tracing", + "url", ] [[package]] @@ -3624,35 +3692,8 @@ dependencies = [ "http-body-util", "hyper 1.7.0", "hyper-util", - "jsonrpsee-core 0.23.2", - "jsonrpsee-types 0.23.2", - "pin-project", - "route-recognizer", - "serde", - "serde_json", - "soketto", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tokio-util", - "tower", - "tracing", -] - -[[package]] -name = "jsonrpsee-server" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e363146da18e50ad2b51a0a7925fc423137a0b1371af8235b1c231a0647328" -dependencies = [ - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-util", - "jsonrpsee-core 0.24.9", - "jsonrpsee-types 0.24.9", + "jsonrpsee-core", + "jsonrpsee-types", "pin-project", "route-recognizer", "serde", @@ -3679,18 +3720,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "jsonrpsee-types" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a8e70baf945b6b5752fc8eb38c918a48f1234daf11355e07106d963f860089" -dependencies = [ - "http 1.3.1", - "serde", - "serde_json", - "thiserror 1.0.69", -] - [[package]] name = "k256" version = "0.13.4" @@ -4151,7 +4180,7 @@ dependencies = [ "rcgen", "ring 0.16.20", "rustls 0.21.12", - "rustls-webpki", + "rustls-webpki 0.101.7", "thiserror 1.0.69", "x509-parser 0.15.1", "yasna", @@ -4205,7 +4234,7 @@ dependencies = [ "soketto", "thiserror 1.0.69", "url", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] @@ -5292,16 +5321,16 @@ dependencies = [ name = "pallet-ghost-consensus" version = "0.0.0" dependencies = [ + "fips204", "frame-benchmarking", "frame-support", "frame-system", "pallet-balances", + "pallet-timestamp", "parity-scale-codec", - "pqcrypto-dilithium", - "pqcrypto-traits", "scale-info", + "serde", "sp-core", - "sp-crypto-hashing", "sp-io", "sp-runtime", ] @@ -5417,7 +5446,7 @@ name = "pallet-transaction-payment-rpc" version = "40.0.0" source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=stable2407#92be93c7cb34d6a2c30639cd17994f589c3cdc60" dependencies = [ - "jsonrpsee 0.23.2", + "jsonrpsee", "pallet-transaction-payment-rpc-runtime-api", "parity-scale-codec", "sp-api", @@ -5874,40 +5903,9 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", -] - -[[package]] -name = "pqcrypto-dilithium" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685de0fa68c6786559d5fcdaa414f0cd68ef3f5d162f61823bd7424cd276726f" -dependencies = [ - "cc", - "glob", - "libc", - "pqcrypto-internals", - "pqcrypto-traits", -] - -[[package]] -name = "pqcrypto-internals" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a326caf27cbf2ac291ca7fd56300497ba9e76a8cc6a7d95b7a18b57f22b61d" -dependencies = [ - "cc", - "dunce", - "getrandom 0.3.4", - "libc", + "zerocopy 0.8.27", ] -[[package]] -name = "pqcrypto-traits" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e851c7654eed9e68d7d27164c454961a616cf8c203d500607ef22c737b51bb" - [[package]] name = "predicates" version = "2.1.5" @@ -6003,8 +6001,6 @@ dependencies = [ [[package]] name = "proc-macro-error" version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", @@ -6255,7 +6251,7 @@ dependencies = [ "pin-project-lite", "quinn-proto 0.9.6", "quinn-udp 0.3.2", - "rustc-hash 1.1.0", + "rustc-hash", "rustls 0.20.9", "thiserror 1.0.69", "tokio", @@ -6274,7 +6270,7 @@ dependencies = [ "pin-project-lite", "quinn-proto 0.10.6", "quinn-udp 0.4.1", - "rustc-hash 1.1.0", + "rustc-hash", "rustls 0.21.12", "thiserror 1.0.69", "tokio", @@ -6290,7 +6286,7 @@ dependencies = [ "bytes", "rand", "ring 0.16.20", - "rustc-hash 1.1.0", + "rustc-hash", "rustls 0.20.9", "slab", "thiserror 1.0.69", @@ -6308,7 +6304,7 @@ dependencies = [ "bytes", "rand", "ring 0.16.20", - "rustc-hash 1.1.0", + "rustc-hash", "rustls 0.21.12", "slab", "thiserror 1.0.69", @@ -6528,7 +6524,7 @@ checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" dependencies = [ "hashbrown 0.13.2", "log", - "rustc-hash 1.1.0", + "rustc-hash", "slice-group-by", "smallvec", ] @@ -6674,12 +6670,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc-hex" version = "2.1.0" @@ -6763,10 +6753,25 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.14", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle 2.6.1", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -6774,7 +6779,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.2.0", + "rustls-pki-types", "schannel", "security-framework", ] @@ -6788,6 +6806,51 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.40", + "rustls-native-certs 0.7.3", + "rustls-platform-verifier-android", + "rustls-webpki 0.102.8", + "security-framework", + "security-framework-sys", + "webpki-roots 0.26.11", + "winapi", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -6798,6 +6861,27 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -7116,6 +7200,31 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "sc-consensus-pow" +version = "0.43.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=stable2407#92be93c7cb34d6a2c30639cd17994f589c3cdc60" +dependencies = [ + "async-trait", + "futures", + "futures-timer", + "log", + "parity-scale-codec", + "parking_lot 0.12.5", + "sc-client-api", + "sc-consensus", + "sp-api", + "sp-block-builder", + "sp-blockchain", + "sp-consensus", + "sp-consensus-pow", + "sp-core", + "sp-inherents", + "sp-runtime", + "substrate-prometheus-endpoint", + "thiserror 1.0.69", +] + [[package]] name = "sc-consensus-slots" version = "0.43.0" @@ -7457,7 +7566,7 @@ dependencies = [ "futures", "futures-timer", "hyper 0.14.32", - "hyper-rustls", + "hyper-rustls 0.24.2", "log", "num_cpus", "once_cell", @@ -7495,7 +7604,7 @@ version = "39.0.0" source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=stable2407#92be93c7cb34d6a2c30639cd17994f589c3cdc60" dependencies = [ "futures", - "jsonrpsee 0.23.2", + "jsonrpsee", "log", "parity-scale-codec", "parking_lot 0.12.5", @@ -7526,7 +7635,7 @@ name = "sc-rpc-api" version = "0.43.0" source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=stable2407#92be93c7cb34d6a2c30639cd17994f589c3cdc60" dependencies = [ - "jsonrpsee 0.23.2", + "jsonrpsee", "parity-scale-codec", "sc-chain-spec", "sc-mixnet", @@ -7553,7 +7662,7 @@ dependencies = [ "http-body-util", "hyper 1.7.0", "ip_network", - "jsonrpsee 0.23.2", + "jsonrpsee", "log", "serde", "serde_json", @@ -7572,7 +7681,7 @@ dependencies = [ "futures", "futures-util", "hex", - "jsonrpsee 0.23.2", + "jsonrpsee", "log", "parity-scale-codec", "parking_lot 0.12.5", @@ -7605,7 +7714,7 @@ dependencies = [ "exit-future", "futures", "futures-timer", - "jsonrpsee 0.23.2", + "jsonrpsee", "log", "parity-scale-codec", "parking_lot 0.12.5", @@ -7725,7 +7834,7 @@ dependencies = [ "parity-scale-codec", "parking_lot 0.12.5", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "sc-client-api", "sc-tracing-proc-macro", "serde", @@ -7964,6 +8073,7 @@ dependencies = [ "core-foundation", "core-foundation-sys", "libc", + "num-bigint", "security-framework-sys", ] @@ -8313,6 +8423,7 @@ dependencies = [ "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", + "getrandom 0.2.16", "pallet-aura", "pallet-balances", "pallet-ghost-consensus", @@ -8329,6 +8440,7 @@ dependencies = [ "sp-block-builder", "sp-consensus-aura", "sp-consensus-grandpa", + "sp-consensus-pow", "sp-core", "sp-genesis-builder", "sp-inherents", @@ -8481,6 +8593,17 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "sp-consensus-pow" +version = "0.40.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=stable2407#92be93c7cb34d6a2c30639cd17994f589c3cdc60" +dependencies = [ + "parity-scale-codec", + "sp-api", + "sp-core", + "sp-runtime", +] + [[package]] name = "sp-consensus-slots" version = "0.40.0" @@ -8717,7 +8840,7 @@ name = "sp-rpc" version = "32.0.0" source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=stable2407#92be93c7cb34d6a2c30639cd17994f589c3cdc60" dependencies = [ - "rustc-hash 1.1.0", + "rustc-hash", "serde", "sp-core", ] @@ -9163,7 +9286,7 @@ dependencies = [ "docify", "frame-system-rpc-runtime-api", "futures", - "jsonrpsee 0.23.2", + "jsonrpsee", "log", "parity-scale-codec", "sc-rpc-api", @@ -9513,6 +9636,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.40", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -9534,9 +9667,9 @@ dependencies = [ "futures-util", "log", "rustls 0.21.12", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tungstenite", ] @@ -10471,6 +10604,24 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -11079,13 +11230,33 @@ dependencies = [ "synstructure 0.13.2", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.27", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1e97b62..c0de9a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,87 +22,98 @@ redundant-lifetimes = "warn" explicit-outlives-requirements = "warn" let-underscore = "allow" +# On Windows MSVC, full debug info pushes the node binary's PDB past the linker's +# internal limit (fatal error LNK1318). The node is meant to be run, not debugged, +# so omit debug info from the dev profile. This also shrinks build size noticeably. +[profile.dev] +debug = false + [workspace.dependencies] # Pin all dependencies to polkadot-sdk version/branch clap = { version = "4.5.13", default-features = false } futures = { version = "0.3.31", default-features = false } -jsonrpsee = { version = "0.24.3", default-features = false } -frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -frame-system-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -substrate-frame-rpc-system = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } +jsonrpsee = { version = "0.23.2", default-features = false } +frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +frame-system-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +substrate-frame-rpc-system = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +frame-metadata-hash-extension = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +serde = { version = "1.0", default-features = false, features = ["derive"] } # Polkadot SDK path dependency polkadot-sdk = { path = "polkadot-sdk" } # Frame and pallet dependencies -frame-support = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -frame-system = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -frame-executive = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-aura = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -pallet-grandpa = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } +frame-support = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +frame-system = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +frame-executive = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-aura = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +pallet-grandpa = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } # Substrate primitives -sp-core = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-io = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-consensus-pow = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-crypto-hashing = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-keyring = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-offchain = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-session = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-std = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-storage = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-version = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-io = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-consensus-pow = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-crypto-hashing = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-keyring = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-offchain = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-session = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-std = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-storage = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-version = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } # Substrate client -sc-cli = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-executor = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-service = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-telemetry = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-keystore = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-client-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-network = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-basic-authorship = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-chain-spec = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-rpc = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-rpc-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-consensus = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sc-offchain = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } +sc-cli = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-executor = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-service = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-telemetry = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-keystore = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-consensus-pow = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-client-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-network = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-basic-authorship = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-chain-spec = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-rpc = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-rpc-api = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-consensus = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sc-offchain = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } -sp-consensus = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } +sp-consensus = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } -frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -frame-system-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -frame-try-runtime = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } +frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +frame-system-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +frame-try-runtime = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } -sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } -sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407" } +sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } +sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2407", default-features = false } codec = { package = "parity-scale-codec", version = "3.7.5", default-features = false, features = ["derive"] } scale-info = { version = "2.11.6", default-features = false, features = ["derive"] } # Local dependencies -pallet-template = { path = "pallets/template" } -pallet-ghost-consensus = { path = "pallets/pallet-ghost-consensus" } +pallet-template = { path = "pallets/template", default-features = false } +pallet-ghost-consensus = { path = "pallets/pallet-ghost-consensus", default-features = false } solochain-template-runtime = { path = "runtime" } + +[patch.crates-io] +proc-macro-error = { path = "vendor/proc-macro-error" } diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md index 49f542c..f4a5fa2 100644 --- a/IMPLEMENTATION_SUMMARY.md +++ b/IMPLEMENTATION_SUMMARY.md @@ -1,358 +1,113 @@ # Ghost Blockchain - Implementation Summary ## Overview -This document summarizes all the improvements and implementations completed for the Ghost blockchain project. - -## What Was Done - -### 1. Comprehensive Unit Tests ✅ -**File:** `pallets/pallet-ghost-consensus/src/tests.rs` (NEW) - -Created a complete test suite with 20+ unit tests covering: -- **Genesis Configuration Tests** - - Initial difficulty and phase setup - -- **Difficulty Adjustment Tests** - - Increase difficulty when blocks are too fast - - Decrease difficulty when blocks are too slow - -- **PoW Verification Tests** - - Enhanced Blake2-256 verification - - SHA-256 verification - - Keccak-256 verification - - Different difficulty levels - -- **Staking Tests** - - Basic staking functionality - - Staking below minimum (error handling) - - Multiple stakes from same account - - Multiple validators staking - -- **Unstaking Tests** - - Basic unstaking - - Unstaking without prior stake (error handling) - - Unstaking more than staked (error handling) - -- **Validator Selection Tests** - - Weighted stake-based selection - - Empty validator set handling - -- **Block Reward Tests** - - 40% miner / 60% staker split calculation - -- **Block Header Validation Tests** - - Block number sequence validation - - Parent hash validation - -- **Phase Transition Tests** - - PoW Mining → PoS Validation → Finalization cycle - -- **Slashing Tests** - - Slashing records storage - - Double-sign reports - - Invalid block reports - - Last active block tracking - -### 2. Enhanced Consensus Implementation ✅ -**File:** `pallets/pallet-ghost-consensus/src/lib.rs` - -Added complete implementation of: - -**Reward Distribution System:** -- `distribute_block_rewards()` - Automatically distributes rewards - - 40% to miners via `deposit_creating()` - - 60% to stakers proportionally by stake weight - - Proper event emission - -**Slashing System:** -- `check_downtime_slashing()` - Monitors validator activity - - Tracks last active block for each validator - - Applies 10% slash for downtime >100 blocks - - Records all slashing events - - Emits ValidatorSlashed events - -**Difficulty Adjustment:** -- `adjust_difficulty()` - Dynamic difficulty adjustment - - Targets 5-second block times - - Adjusts based on actual vs target block time - - Emits DifficultyAdjusted events - -**Automatic Hooks:** -- `on_initialize()` - Runs at block start - - Checks downtime slashing every 10 blocks - - Adjusts difficulty every 100 blocks - -- `on_finalize()` - Runs at block end - - Transitions back to PoW Mining phase - -### 3. CLI Mining Implementation ✅ -**File:** `node/src/miner.rs` (NEW) - -Created a fully functional multi-threaded PoW miner: - -**Features:** -- Multi-threaded mining (configurable thread count) -- Enhanced Blake2-256 double-hashing (ASIC-resistant) -- Real-time hash rate calculation -- Mining statistics tracking -- Graceful shutdown support -- Thread-safe atomic operations - -**Mining Stats:** -- Hashes computed -- Blocks found -- Hash rate (H/s) -- Elapsed time - -**Usage:** -```bash -ghost-node ghost mine --threads 4 --difficulty 1000000000000 -``` - -### 4. CLI Command Enhancements ✅ -**File:** `node/src/command.rs` - -Implemented all Ghost-specific CLI commands: - -**`ghost mine`:** -- Actual PoW mining with configurable threads -- Displays real-time mining progress -- Shows nonce when solution found -- Instructions for submitting blocks - -**`ghost stake`:** -- Clear instructions for staking via Polkadot.js Apps -- Shows extrinsic format -- Multiple submission methods - -**`ghost unstake`:** -- Clear instructions for unstaking -- Proper error guidance - -**`ghost balance`:** -- Shows default development accounts (Alice, Bob) -- Account addresses and genesis balances -- Instructions for checking live balances - -**`ghost status`:** -- Comprehensive consensus information -- Detailed mode with: - - Slashing conditions and percentages - - Phase flow explanation - - Network information - -**`ghost validators`:** -- Default genesis validators -- Instructions for live validator queries - -### 5. RPC Endpoints for Ghost Consensus ✅ -**File:** `pallets/pallet-ghost-consensus/src/rpc.rs` (NEW) - -Created custom RPC API for Ghost consensus queries: - -**RPC Methods:** -- `ghost_getDifficulty` - Get current mining difficulty -- `ghost_getCurrentPhase` - Get current consensus phase (PoW/PoS/Finalization) -- `ghost_getValidatorStake` - Get stake amount for specific validator -- `ghost_getAllValidators` - Get all validators with their stakes -- `ghost_getSlashingRecords` - Get number of slashing records - -**Runtime API:** -- Defined `GhostConsensusRuntimeApi` trait -- Implemented all RPC methods in runtime (apis.rs) -- Full integration with Substrate RPC layer - -### 6. Complete Functions Module ✅ -**File:** `pallets/pallet-ghost-consensus/src/functions.rs` - -Already implemented (verified complete): -- `calculate_difficulty_adjustment()` - Dynamic difficulty -- `verify_pow()` - Basic Blake2-256 PoW -- `verify_pow_enhanced()` - Double-hash Blake2-256 -- `verify_pow_sha256()` - Bitcoin-style PoW -- `verify_pow_keccak()` - Ethereum-style PoW -- `select_pos_validator()` - Weighted stake selection -- `calculate_block_reward()` - 40/60 split calculation -- `validate_block_header()` - Full header validation -- `distribute_rewards()` - Reward distribution logic - -### 7. Storage and Types ✅ - -**Storage Items:** -- `Difficulty` - Current mining difficulty -- `CurrentPhase` - Consensus phase -- `BlockHeaders` - Block headers storage -- `ValidatorStakes` - Validator stake amounts -- `LastActiveBlock` - For downtime tracking -- `DoubleSignReports` - Double-signing reports -- `InvalidBlockReports` - Invalid block reports -- `SlashingRecords` - Complete slashing history - -**Events:** -- `BlockMined` - New block mined -- `ValidatorSelected` - Validator selected for PoS -- `RewardsDistributed` - Block rewards distributed -- `ValidatorSlashed` - Validator slashed with reason -- `DifficultyAdjusted` - Difficulty changed - -**Errors:** -- Complete error handling for all edge cases -- Proper validation at every step - -## Architecture Improvements - -### Consensus Flow -``` -1. PoW Mining Phase - ↓ - Miners compete with Enhanced Blake2-256 - ↓ -2. PoS Validation Phase - ↓ - Validators selected by weighted stake - ↓ -3. Finalization Phase - ↓ - Rewards distributed (40% miner, 60% stakers) - ↓ - Return to PoW Mining -``` - -### Slashing Conditions -- **Double Signing:** 100% stake slash -- **Invalid Block:** 50% stake slash -- **Downtime (>100 blocks):** 10% stake slash - -### Reward Economics -- **Total Reward:** 10 Ghost tokens per block -- **Miner:** 4 Ghost tokens (40%) -- **Stakers:** 6 Ghost tokens (60%, distributed proportionally) - -## How to Use - -### Build the Project -```bash -cargo build --release --bin ghost-node -``` - -### Run Development Node -```bash -./target/release/ghost-node --dev -``` - -### Mine Blocks -```bash -./target/release/ghost-node ghost mine --threads 4 -``` - -### Check Status -```bash -./target/release/ghost-node ghost status --detailed -``` - -### Stake Tokens (via Polkadot.js Apps) -1. Connect to ws://localhost:9944 -2. Navigate to Developer → Extrinsics -3. Submit: `ghostConsensus.stake(amount)` - -### Query via RPC -```javascript -// Using Polkadot.js API -const difficulty = await api.rpc.ghost.getDifficulty(); -const phase = await api.rpc.ghost.getCurrentPhase(); -const validators = await api.rpc.ghost.getAllValidators(); -``` - -## Testing - -### Run Unit Tests -```bash -cargo test -p pallet-ghost-consensus -``` - -### Run All Tests -```bash -cargo test -``` - -### Test Specific Function -```bash -cargo test test_staking_basic -- --nocapture -``` - -## Next Steps for Production - -### Phase 1: Build Environment Setup -The current build error is due to Windows linker configuration, not code issues. To fix: -1. Install Visual Studio Build Tools -2. Ensure "C++ build tools" workload is selected -3. Or use WSL2 (Windows Subsystem for Linux) for Linux-based builds - -### Phase 2: Integration Testing -1. Set up multi-node test network -2. Test consensus across multiple nodes -3. Verify validator selection randomness -4. Test slashing under various conditions -5. Load testing for 5-second block times - -### Phase 3: Security Audit -1. Review reward distribution math -2. Verify slashing conditions are fair -3. Test for double-spend vulnerabilities -4. Review PoW ASIC resistance -5. Analyze stake-grinding attacks - -### Phase 4: Optimization -1. Benchmark weight calculations -2. Optimize storage access patterns -3. Profile mining performance -4. Review memory usage - -### Phase 5: Documentation -1. API documentation (cargo doc) -2. User guide for validators -3. Mining pool integration guide -4. Network deployment guide - -## Files Created/Modified - -### New Files -- `pallets/pallet-ghost-consensus/src/tests.rs` - Complete test suite -- `pallets/pallet-ghost-consensus/src/rpc.rs` - RPC interface -- `node/src/miner.rs` - Mining implementation -- `IMPLEMENTATION_SUMMARY.md` - This file - -### Modified Files -- `pallets/pallet-ghost-consensus/src/lib.rs` - Enhanced with hooks and reward distribution -- `node/src/command.rs` - Implemented all CLI commands -- `node/src/main.rs` - Added miner module -- `runtime/src/apis.rs` - Added Ghost RPC runtime API - -## Summary - -All requested improvements have been successfully implemented: - -✅ **20+ comprehensive unit tests** covering all consensus functionality -✅ **Complete validator selection** using weighted stake algorithm -✅ **Full reward distribution** with 40/60 miner/staker split -✅ **Automatic slashing logic** for double-signing, invalid blocks, and downtime -✅ **Phase transition hooks** with automatic difficulty adjustment -✅ **Fully functional CLI mining** with multi-threading -✅ **Complete CLI commands** with user-friendly output -✅ **Custom RPC endpoints** for consensus queries - -The Ghost blockchain is now feature-complete and ready for integration testing and deployment once the build environment is properly configured. - -## Build Note - -The compilation error encountered is a **Windows linker configuration issue**, NOT a code problem. The error: -``` -link: extra operand 'C:\\Users\\faris\\ghost-blockhain\\target\\...' -``` - -indicates that the `link.exe` linker expects different arguments. This is resolved by: -1. Installing Visual Studio Build Tools with C++ workload, OR -2. Using WSL2 with a Linux toolchain, OR -3. Using rustup to install the GNU toolchain: `rustup default stable-x86_64-pc-windows-gnu` - -All code is syntactically correct and logically sound. The implementations follow Substrate best practices and are production-ready. + +This summary reflects the implementation currently present in the repository. + +## Consensus Architecture + +The node runs real Proof-of-Work block authoring via `sc-consensus-pow`. Aura and GRANDPA have been removed from both the node and the runtime. The canonical chain is selected by accumulated PoW (longest-/heaviest-chain); finality is probabilistic, not BFT. + +Block authoring is implemented in `node/src/service.rs` and `node/src/pow.rs`: + +- Hash function: double-Blake2-256 over `pre_hash || nonce` (64-bit little-endian nonce) +- Difficulty convention: numerically larger `U256` value = harder; a hash (as a big-endian 256-bit integer) is valid iff it is `<= U256::MAX / difficulty` +- Fork choice: `sc-consensus-pow` sums total difficulty for chain selection +- Difficulty value: read from the runtime each block via `sp_consensus_pow::DifficultyApi`, which exposes the value retargeted on-chain by `pallet-ghost-consensus` using timestamp-based retargeting toward a target block time + +## Ghost Pallet — Proof-of-Stake Economic Layer + +`pallet-ghost-consensus` implements the PoS validator economics layer. It is wired into the live runtime and exposes `DifficultyApi` to the node. + +### Pallet lifecycle + +1. A miner submits a Ghost header that passes pallet PoW validation. +2. The pallet enters PoS validation for the submitted block. +3. A validator is selected from stakers by stake weight, seeded by the parent hash. +4. The pallet records the validation outcome, distributes rewards, and returns to PoW mode. + +### Core pallet capabilities + +- Stake and unstake flows with a minimum-stake floor +- Stake-weighted validator selection (seeded by parent hash, stored at submit time) +- Block reward split: 40% to the miner, 60% distributed among stakers; dust handled without loss +- Evidence-gated slashing (`report_misbehavior` requires structured `MisbehaviorEvidence`): double-sign, invalid-block, downtime, and an `Other` proof-hash path; slashed funds are burned +- Validation-timeout recovery: `check_validation_timeout()` returns the pallet to PoW mode if validation stalls past `MaxValidationBlocks`; emits `ValidationTimedOut` +- Bounded state: `MaxValidators` cap on validator membership; `MaxSlashingRecords` cap on slashing history + +### Tests + +37 pallet unit tests pass, covering staking, validator selection, reward splitting, slashing evidence validation, bounded validator counts, and timeout recovery. + +## Post-Quantum Signatures (ML-DSA, NIST FIPS 204) + +On-chain ML-DSA signature verification is implemented in `pallets/pallet-ghost-consensus/src/pq_verify.rs` using the pure-Rust `fips204` crate compiled into the no\_std Wasm runtime. + +Supported parameter sets: ML-DSA-44 (NIST level 2), ML-DSA-65 (level 3), ML-DSA-87 / Dilithium-5 (level 5). + +Pallet extrinsics: + +- `register_ml_dsa_key`: validators register an ML-DSA public key on-chain +- `verify_pq_signature`: general-purpose extrinsic for verifying a real ML-DSA signature against a registered key +- `validate_block`: requires a valid ML-DSA signature from the selected validator when that validator has a registered key + +Key generation and signing are off-chain operations; the runtime only verifies. Tests include real keygen/sign/verify round-trips and tamper-detection tests. + +Ordinary extrinsics still use `MultiSignature` (sr25519/ed25519/ecdsa). ML-DSA is an additional registered validator/attestation path, not a replacement for the account signature scheme. + +## Post-Quantum Encryption Module (ML-KEM-1024, NIST FIPS 203) + +`node/src/pq_encrypt.rs` implements ML-KEM-1024 key encapsulation (`fips203` crate) combined with ChaCha20-Poly1305 AEAD (`chacha20poly1305` crate) for application-layer payload encryption and operator tooling. + +Key lengths (FIPS 203, ML-KEM-1024): encapsulation key 1 568 bytes, decapsulation key 3 168 bytes, ciphertext 1 568 bytes, shared secret 32 bytes. + +Security boundary: this module provides post-quantum protection for payloads and operator utilities. It does **not** replace the libp2p Noise/X25519 transport handshake. Node-to-node transport remains classical (X25519 + ChaCha20-Poly1305 Noise) because the `stable2407` polkadot-sdk does not include a PQ-Noise variant. + +## Hardening State + +### Bounded state + +- Validator membership is capped with `MaxValidators`. +- Slashing history uses a bounded vector with `MaxSlashingRecords`. +- Runtime config exposes those limits in `runtime/src/configs/mod.rs`. + +### Validation round safety + +- Submitted blocks are tracked in `PendingValidationBlock`. +- Validation windows are tracked with `PhaseStartedAt`. +- `check_validation_timeout()` returns to PoW mode if validation stalls. +- `ValidationTimedOut` event is emitted on timeout recovery. + +### Evidence-gated slashing + +- `report_misbehavior` requires structured `MisbehaviorEvidence`. +- Evidence is validated before stake reduction via `validate_misbehavior_evidence`. +- Slashed funds are burned; evidence attribution is recorded. + +## Honesty Boundaries + +- Node-to-node transport is classical libp2p with Noise/X25519; it is not post-quantum. +- `MultiSignature` remains the account signature scheme; ML-DSA is an additional validator path. +- Finality is probabilistic longest-chain PoW; there is no BFT finality gadget. +- No external security audit has been performed. The implementation is tested and functional; an audit and multi-node adversarial testing remain before a public mainnet. + +## What Remains Before a Production Claim + +1. PQ transport: replacing libp2p Noise/X25519 with a PQ-Noise variant +2. External cryptography and integration audit +3. Multi-node adversarial testing under realistic validator-network conditions +4. Production key management, rotation, and operator runbooks +5. Benchmarks and DoS review for PQ signature sizes and verification load under worst-case conditions + +## Important Files + +- `node/src/service.rs`: PoW node wiring (`sc-consensus-pow`, no Aura/GRANDPA) +- `node/src/pow.rs`: double-Blake2-256 PoW algorithm, `meets_difficulty`, `GhostPow` +- `node/src/pq_encrypt.rs`: ML-KEM-1024 + ChaCha20-Poly1305 encryption module +- `node/src/command.rs`: Ghost CLI status and helper commands +- `runtime/src/configs/mod.rs`: Ghost pallet runtime limits and reward percentages +- `pallets/pallet-ghost-consensus/src/lib.rs`: pallet storage, calls, hooks, and timeout handling +- `pallets/pallet-ghost-consensus/src/pq_verify.rs`: ML-DSA verification (FIPS 204, `no_std` Wasm) +- `pallets/pallet-ghost-consensus/src/functions.rs`: header validation, reward helpers, slashing evidence +- `pallets/pallet-ghost-consensus/src/types.rs`: structured slashing evidence types, `PqAlgorithm` enum +- `pallets/pallet-ghost-consensus/src/tests.rs`: 37 pallet unit tests diff --git a/RALPH_PROGRESS.md b/RALPH_PROGRESS.md new file mode 100644 index 0000000..9937f70 --- /dev/null +++ b/RALPH_PROGRESS.md @@ -0,0 +1,66 @@ +# Ghost Blockchain — Implementation Progress + +Goal: a technically complete, working hybrid PoW + PoS chain with real post-quantum +cryptography. Audits/legal/liquidity are out of scope as launch *gates* only. + +## ✅ VERIFIED: a live PoW blockchain that authors & imports blocks + +`./target/debug/ghost-node --dev --tmp` boots and produces blocks: +``` +🙌 Starting consensus session on top of parent ... (#0) +🎁 Prepared block for proposing at 1 +✅ Successfully mined block on top of: ... +🏆 Imported #1 ... #2 ... #3 ... #21 (best: #21 in ~24s) +``` +Real Proof-of-Work authoring via `sc-consensus-pow`; the runtime (with on-chain ML-DSA-87, +no Aura/GRANDPA) executes each block. Benign `seal is invalid` lines are multi-thread nonce +races (one thread wins; stale submissions are correctly rejected). + +## Phase 1 — Pallet (PoS + PQ signatures) ✅ 37/37 tests pass +Overflow-safe math, conventional PoW difficulty (full U256 vs canonical), real timestamp +difficulty retargeting, slash-and-burn, unstake floor + atomicity, stored validator +selection, slash attribution, header pruning. Real ML-DSA-87 ("Dilithium-5", FIPS 204) +on-chain verification (`fips204`, no_std/Wasm): `register_ml_dsa_key`, `verify_pq_signature`, +mandatory PQ signature in `validate_block` for registered validators. + +## Phase 2 — Real PoW node ✅ builds, boots, mines +Removed Aura/GRANDPA from node+runtime; `sp_consensus_pow::DifficultyApi` exposes the pallet +difficulty; `node/src/pow.rs` (GhostPow: double-Blake2 over pre_hash||nonce, U256 target); +`node/src/service.rs` rewrite (PowBlockImport + import_queue + start_mining_worker + OS-thread +CPU miners, longest-chain). Node binary links and runs (see above). + +## Phase 3 — PQ encryption (ML-KEM-1024 / Kyber) 🔄 integrating +`node/src/pq_encrypt.rs`: real ML-KEM-1024 (FIPS 203) key encapsulation + ChaCha20-Poly1305 +AEAD (`fips203` + `chacha20poly1305`). Final node build verifies it. + +## Re-audit fixes applied +HIGH-1 (InvalidBlock evidence uses header's own difficulty, not current → no false slash +after retarget), HIGH-3 (validator signature bound to immutable header fields + block- +specific, no cross-block replay), HIGH-5 (drop LastActiveBlock ghost entries on zero-stake), +MEDIUM-3 (no-staker block → miner gets full reward, nothing dropped), MEDIUM-6 (unstake +returns funds before mutating records). + +## Hardening pass 2 (this round) +- Replay guard: `verify_pq_signature` rejects re-submitting an already-recorded + (attester, statement) attestation (`AttestationAlreadyRecorded`). New test. +- No-staker immediate finality: when the staker set is empty, `submit_block` finalizes the + PoW block at once (miner gets the full reward) and stays in `PowMining`, instead of + entering `PosValidation` and waiting for the validation timeout. New test. +- Single PoW source of truth: the `mine` CLI demo now runs the exact node work function + (`crate::pow::{pow_hash, meets_difficulty}`, conventional difficulty) instead of a + separate inverted-difficulty hash. CLI `--difficulty` now matches the chain's convention. +- Stale doc fixed: the pallet's module doc no longer claims Aura/GRANDPA authoring. + +## Known limitations (honest, not launch-blocking for a devnet) +- Node-to-node transport is classical libp2p Noise/X25519 (stable2407 has no PQ-Noise). + PQ is signatures (ML-DSA) + an app-layer encryption module (ML-KEM), not a transport swap. +- Block subsidy is inflationary by design (no hard cap); decide a monetary policy before mainnet. +- Weights: `weights.rs` `SubstrateWeight` is wired into the runtime config (replacing the + `()` placeholder). Each dispatchable's weight is operation-grounded — real `DbWeight` read/write + counts (incl. staker-set iteration bounded by `MaxValidators`) plus a generous compute allowance, + with a ~300µs on-chain ML-DSA-87 verify charge on `validate_block`/`verify_pq_signature`. It + deliberately over-estimates; empirical `frame-benchmarking` numbers are still recommended before mainnet. +- No external security audit; probabilistic (longest-chain) PoW finality, not BFT. + +## Build env (Windows): rustc 1.90 stable-msvc; clang/cmake/perl on PATH + LIBCLANG_PATH; +jsonrpsee pinned 0.23.2 (matches stable2407); ahash 0.8.11; getrandom custom backend in runtime. diff --git a/README.md b/README.md index 27ceab3..23e944c 100644 --- a/README.md +++ b/README.md @@ -1,318 +1,105 @@ # Ghost Blockchain -🚀 **Ghost** - A next-generation blockchain with hybrid Proof-of-Work (PoW) and Proof-of-Stake (PoS) consensus, designed for speed, security, and energy efficiency. +Ghost is a Substrate-based chain with a hybrid Proof-of-Work / Proof-of-Stake consensus engine and on-chain post-quantum signature verification. -## 🌟 Features +## Current State -- **Hybrid Consensus**: Combines PoW security with PoS efficiency -- **Entropy-Steered Consensus (ESC)**: Dynamics difficulty adjustment based on network decentralization (Shannon Entropy) -- **Quantum-Resistant Finality**: Post-Quantum Cryptography (PQC) using Crystals-Dilithium signatures for block finalization -- **Fast Block Times**: 5-second block production -- **ASIC-Resistant Mining**: Enhanced Blake2-256 algorithm -- **Weighted PoS**: Stake-based validator selection -- **Energy Efficient**: Optimized for low computational cost -- **Built on Substrate**: Leverages Polkadot SDK for robustness +- Block authoring is real Proof-of-Work via `sc-consensus-pow` (`node/src/service.rs`, `node/src/pow.rs`). Aura and GRANDPA have been removed from both the node and runtime. Finality is probabilistic longest-/heaviest-chain PoW; there is no BFT finality gadget. +- `pallet-ghost-consensus` implements the PoS economic layer: staking/unstaking with a minimum-stake floor, stake-weighted validator selection, a 40%/60% miner/staker reward split, evidence-gated slashing that burns funds, and validation-timeout recovery. 37 pallet unit tests pass. +- On-chain ML-DSA (NIST FIPS 204) signature verification is implemented and tested in the runtime Wasm (`pallets/pallet-ghost-consensus/src/pq_verify.rs`, `fips204` crate). Validators may register an ML-DSA public key; `validate_block` requires a valid ML-DSA signature from the selected validator when a key is registered. +- Node-side ML-KEM-1024 + ChaCha20-Poly1305 payload encryption is implemented (`node/src/pq_encrypt.rs`, NIST FIPS 203). +- No external security audit has been performed. The implementation is tested and functional; an audit and multi-node adversarial testing remain before a public mainnet. -## 🎯 Consensus Mechanism +## What Works -Ghost uses a revolutionary hybrid approach: +- Real PoW block production and import via `sc-consensus-pow` with double-Blake2-256 (`pre_hash || nonce`), `U256` difficulty, and difficulty retargeting in `pallet-ghost-consensus` +- Pallet-level staking, validator selection, reward distribution, and slashing logic with 37 passing unit tests +- On-chain ML-DSA-87 (Dilithium-5, FIPS 204) signature verification inside the no\_std Wasm runtime +- Node-side ML-KEM-1024 key encapsulation + ChaCha20-Poly1305 AEAD for application-layer payload encryption +- Runtime guardrails: bounded validator count, bounded slashing history, evidence-gated slashing, validation timeout recovery -1. **PoW Phase**: Miners compete using ASIC-resistant Blake2-256 -2. **ESC Layer**: Difficulty is dynamically steered by network entropy to prevent centralization -3. **PoS Phase**: Validators selected by stake weight sign blocks using PQC (Dilithium-5) -4. **Reward Distribution**: 40% to miners, 60% to stakers -5. **Slashing**: Penalties for double-signing, invalid blocks, and downtime +## Honesty Boundaries -## 📊 Key Specifications +- Node-to-node transport is still classical: libp2p with Noise/X25519. The `stable2407` polkadot-sdk does not include a PQ-Noise variant, so **the transport layer is not post-quantum**. +- Ordinary extrinsics still use `MultiSignature` (sr25519/ed25519/ecdsa). ML-DSA is an additional registered validator/attestation path, not a replacement for the account signature scheme. +- PoW finality is probabilistic (longest-chain), not BFT. +- No external security audit has been completed. Do not describe this as production-ready for a public mainnet. -- **Block Time**: 5 seconds -- **PoW Algorithm**: Enhanced Blake2-256 (ASIC-resistant) -- **Entropy Threshold**: 4.0 (for ESC difficulty steering) -- **Signature Scheme**: Dilithium-5 (Post-Quantum Secure) -- **Consensus**: Hybrid PoW + Weighted PoS + ESC -- **Token**: Ghost (GHTM) -- **Block Reward**: 10 GHOST per block -- **Minimum Stake**: 1 GHOST token +## What Remains -### Testing & Simulation +- PQ transport (replacing libp2p Noise/X25519 with a PQ-Noise variant) +- External cryptography and integration audit +- Multi-node adversarial testing +- Production key management tooling and operator runbooks -Before running the full node, you can verify the consensus math using the provided scripts: +See `docs/pqc-roadmap.md` for the staged implementation plan with current completion status. -```bash -# Simulate Entropy-Steered Consensus difficulty adjustment -python3 scripts/simulate_esc.py - -# Test PQC Dilithium-5 signature verification (requires Rust + pqcrypto) -rustc scripts/test_pqc.rs --extern pqcrypto_dilithium -L ./target/debug/deps -./test_pqc -``` - -## 🛠️ Build & Installation - -Depending on your operating system and Rust version, there might be additional -packages required to compile this template. Check the -[Install](https://docs.substrate.io/install/) instructions for your platform for -the most common dependencies. Alternatively, you can use one of the [alternative -installation](#alternatives-installations) options. - -Fetch solochain template code: - -```sh -git clone https://github.com/paritytech/polkadot-sdk-solochain-template.git solochain-template - -cd solochain-template -``` - -### Build - -🔨 Use the following command to build the Ghost node: +## Build ```sh -# Build in release mode (recommended for production) -cargo build --release --bin ghost-node - -# Build in debug mode (faster for development) cargo build --bin ghost-node ``` -### Test the Build - -After building, test the Ghost-specific CLI commands: - -```sh -# Test main help -./target/release/ghost-node --help - -# Test Ghost commands -./target/release/ghost-node ghost --help - -# Test consensus status -./target/release/ghost-node ghost status --detailed - -# Test mining command -./target/release/ghost-node ghost mine --threads 4 -``` - -You can generate and view the [Rust -Docs](https://doc.rust-lang.org/cargo/commands/cargo-doc.html) for this template -with this command: - -```sh -cargo +nightly doc --open -``` - -### Single-Node Development Chain - -The following command starts a single-node Ghost development chain: - -```sh -# Start development chain -./target/release/ghost-node --dev - -# Start with custom base path -./target/release/ghost-node --dev --base-path ./ghost-chain-data -``` - -To purge the development chain's state, run the following command: +## Test ```sh -./target/release/ghost-node purge-chain --dev +cargo test -p pallet-ghost-consensus --target-dir .cargo-target ``` -To start the development chain with detailed logging, run the following command: +## Run A Local Node ```sh -RUST_BACKTRACE=1 ./target/release/ghost-node -ldebug --dev -``` - -### Ghost-Specific Commands - -Once your node is running, you can interact with it using Ghost-specific commands: - -```sh -# Check consensus status -./target/release/ghost-node ghost status --detailed - -# Start mining (when implemented) -./target/release/ghost-node ghost mine --threads 4 - -# Stake tokens for validation -./target/release/ghost-node ghost stake --amount 1000 - -# Check your balance -./target/release/ghost-node ghost balance +cargo run --bin ghost-node -- --dev ``` -Development chains: - -- Maintain state in a `tmp` folder while the node is running. -- Use the **Alice** and **Bob** accounts as default validator authorities. -- Use the **Alice** account as the default `sudo` account. -- Are preconfigured with a genesis state (`/node/src/chain_spec.rs`) that - includes several pre-funded development accounts. +## Ghost CLI Wallet & Helpers - -To persist chain state between runs, specify a base path by running a command -similar to the following: +A running node exposes JSON-RPC (default `http://127.0.0.1:9944`). The `ghost` subcommands +query live chain state and sign + submit real extrinsics against it (sr25519, using the +runtime's own extrinsic types). All accept `--rpc-url` to target a non-default endpoint. ```sh -# Create a folder to use as the db base path -$ mkdir ghost-chain-state - -# Use of that folder to store the chain state -$ ./target/release/ghost-node --dev --base-path ./ghost-chain-state - -# Check the folder structure created inside the base path after running the chain -$ ls ./ghost-chain-state -chains -$ ls ./ghost-chain-state/chains/ -dev -$ ls ./ghost-chain-state/chains/dev -db keystore network +# Read-only live queries +cargo run --bin ghost-node -- ghost balance --account //Alice +cargo run --bin ghost-node -- ghost validators +cargo run --bin ghost-node -- ghost status --detailed + +# Signed + submitted transactions (default signer //Alice; override with --account ) +cargo run --bin ghost-node -- ghost stake --amount 3000000000000 +cargo run --bin ghost-node -- ghost unstake --amount 1000000000000 +cargo run --bin ghost-node -- ghost transfer --to //Bob --amount 5000000000000 + +# Post-quantum: generate an ML-DSA-87 (FIPS 204) keypair, then register it on-chain so +# the signer's validator attestations are checked against it. +cargo run --bin ghost-node -- ghost pq-keygen --out my-validator-key +cargo run --bin ghost-node -- ghost register-key --key my-validator-key.pub + +# Post-quantum encryption: ML-KEM-1024 (FIPS 203) key encapsulation + ChaCha20-Poly1305 +cargo run --bin ghost-node -- ghost pq-kem-keygen --out recipient +cargo run --bin ghost-node -- ghost pq-encrypt --to recipient.ek --in secret.txt --out secret.enc +cargo run --bin ghost-node -- ghost pq-decrypt --dk recipient.dk --in secret.enc --out secret.out + +# Local PoW benchmark demo (runs the real work function; does NOT submit to the chain) +cargo run --bin ghost-node -- ghost mine --threads 2 ``` -### Ghost Consensus Pallet - -The core of Ghost's hybrid consensus is implemented in the `pallet-ghost-consensus`: - -- **Location**: `pallets/pallet-ghost-consensus/` -- **Features**: - - PoW mining with ASIC-resistant Blake2-256 - - **Entropy-Steered Consensus (ESC)** for decentralization - - **Quantum-Resistant Finality** via Dilithium-5 - - Weighted PoS validator selection - - Block reward distribution (40/60 split) - - Slashing for misbehavior - - Staking functionality - -Key files: -- `src/lib.rs`: Main pallet implementation -- `src/types.rs`: Consensus data structures -- `src/functions.rs`: Core consensus algorithms -- `src/consensus.rs`: Consensus engine integration - -### Connect with Polkadot-JS Apps Front-End - -After you start the node template locally, you can interact with it using the -hosted version of the [Polkadot/Substrate -Portal](https://polkadot.js.org/apps/#/explorer?rpc=ws://localhost:9944) -front-end by connecting to the local node endpoint. A hosted version is also -available on [IPFS](https://dotapps.io/). You can -also find the source code and instructions for hosting your own instance in the -[`polkadot-js/apps`](https://github.com/polkadot-js/apps) repository. - -### Multi-Node Local Testnet - -If you want to see the multi-node consensus algorithm in action, see [Simulate a -network](https://docs.substrate.io/tutorials/build-a-blockchain/simulate-network/). - -## Template Structure - -A Substrate project such as this consists of a number of components that are -spread across a few directories. - -### Node - -A blockchain node is an application that allows users to participate in a -blockchain network. Substrate-based blockchain nodes expose a number of -capabilities: - -- Networking: Substrate nodes use the [`libp2p`](https://libp2p.io/) networking - stack to allow the nodes in the network to communicate with one another. -- Consensus: Blockchains must have a way to come to - [consensus](https://docs.substrate.io/fundamentals/consensus/) on the state of - the network. Substrate makes it possible to supply custom consensus engines - and also ships with several consensus mechanisms that have been built on top - of [Web3 Foundation - research](https://research.web3.foundation/Polkadot/protocols/NPoS). -- RPC Server: A remote procedure call (RPC) server is used to interact with - Substrate nodes. - -There are several files in the `node` directory. Take special note of the -following: - -- [`chain_spec.rs`](./node/src/chain_spec.rs): A [chain - specification](https://docs.substrate.io/build/chain-spec/) is a source code - file that defines a Substrate chain's initial (genesis) state. Chain - specifications are useful for development and testing, and critical when - architecting the launch of a production chain. Take note of the - `development_config` and `testnet_genesis` functions. These functions are - used to define the genesis state for the local development chain - configuration. These functions identify some [well-known - accounts](https://docs.substrate.io/reference/command-line-tools/subkey/) and - use them to configure the blockchain's initial state. -- [`service.rs`](./node/src/service.rs): This file defines the node - implementation. Take note of the libraries that this file imports and the - names of the functions it invokes. In particular, there are references to - consensus-related topics, such as the [block finalization and - forks](https://docs.substrate.io/fundamentals/consensus/#finalization-and-forks) - and other [consensus - mechanisms](https://docs.substrate.io/fundamentals/consensus/#default-consensus-models) - such as Aura for block authoring and GRANDPA for finality. - - -### Runtime - -In Substrate, the terms "runtime" and "state transition function" are analogous. -Both terms refer to the core logic of the blockchain that is responsible for -validating blocks and executing the state changes they define. The Substrate -project in this repository uses -[FRAME](https://docs.substrate.io/learn/runtime-development/#frame) to construct -a blockchain runtime. FRAME allows runtime developers to declare domain-specific -logic in modules called "pallets". At the heart of FRAME is a helpful [macro -language](https://docs.substrate.io/reference/frame-macros/) that makes it easy -to create pallets and flexibly compose them to create blockchains that can -address [a variety of needs](https://substrate.io/ecosystem/projects/). - -Review the [FRAME runtime implementation](./runtime/src/lib.rs) included in this -template and note the following: - -- This file configures several pallets to include in the runtime. Each pallet - configuration is defined by a code block that begins with `impl - $PALLET_NAME::Config for Runtime`. -- The pallets are composed into a single runtime by way of the - [#[runtime]](https://paritytech.github.io/polkadot-sdk/master/frame_support/attr.runtime.html) - macro, which is part of the [core FRAME pallet - library](https://docs.substrate.io/reference/frame-pallets/#system-pallets). - -### Pallets - -The runtime in this project is constructed using many FRAME pallets that ship -with [the Substrate -repository](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame) and a -template pallet that is [defined in the -`pallets`](./pallets/template/src/lib.rs) directory. - -A FRAME pallet is comprised of a number of blockchain primitives, including: - -- Storage: FRAME defines a rich set of powerful [storage - abstractions](https://docs.substrate.io/build/runtime-storage/) that makes it - easy to use Substrate's efficient key-value database to manage the evolving - state of a blockchain. -- Dispatchables: FRAME pallets define special types of functions that can be - invoked (dispatched) from outside of the runtime in order to update its state. -- Events: Substrate uses - [events](https://docs.substrate.io/build/events-and-errors/) to notify users - of significant state changes. -- Errors: When a dispatchable fails, it returns an error. - -Each pallet has its own `Config` trait which serves as a configuration interface -to generically define the types and parameters it depends on. - -## Alternatives Installations - -Instead of installing dependencies and building this source directly, consider -the following alternatives. - -### Nix - -Install [nix](https://nixos.org/) and -[nix-direnv](https://github.com/nix-community/nix-direnv) for a fully -plug-and-play experience for setting up the development environment. To get all -the correct dependencies, activate direnv `direnv allow`. - -### Docker - -Please follow the [Substrate Docker instructions -here](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/docker/README.md) to -build the Docker container with the Substrate Node Template binary. +`balance`/`validators` decode `System.Account` and `GhostConsensus.ValidatorStakes` from +state (`validators` also reports each validator's ML-DSA key status); `stake`/`unstake`/ +`transfer`/`register-key` build, sign, and submit via `author_submitExtrinsic`. `pq-keygen` +writes a real ML-DSA-87 keypair (`.pub` 2592 bytes / `.sec` 4896 bytes; keep the +secret file safe). `pq-kem-keygen`/`pq-encrypt`/`pq-decrypt` provide offline ML-KEM-1024 + +ChaCha20-Poly1305 hybrid encryption (`.ek` 1568 / `.dk` 3168 bytes) and need no +running node. + +## Important Paths + +- `node/src/service.rs`: PoW node wiring (`sc-consensus-pow`, no Aura/GRANDPA) +- `node/src/pow.rs`: double-Blake2-256 PoW algorithm and difficulty check +- `node/src/pq_encrypt.rs`: ML-KEM-1024 + ChaCha20-Poly1305 encryption module +- `node/src/command.rs`: Ghost helper CLI +- `runtime/src/lib.rs`: runtime composition +- `runtime/src/configs/mod.rs`: runtime pallet configs and limits +- `pallets/pallet-ghost-consensus/src/lib.rs`: pallet storage, calls, hooks, and timeout handling +- `pallets/pallet-ghost-consensus/src/pq_verify.rs`: ML-DSA verification (FIPS 204, `no_std` Wasm) +- `pallets/pallet-ghost-consensus/src/functions.rs`: header validation, reward helpers, slashing evidence +- `pallets/pallet-ghost-consensus/src/tests.rs`: 37 pallet unit tests diff --git a/consensus.md b/consensus.md deleted file mode 100644 index fa49f0f..0000000 --- a/consensus.md +++ /dev/null @@ -1,13 +0,0 @@ -# Ghost Chain Proto - Hybrid Consensus Logic - -## Summary -Hybrid PoW (for distribution/security) + PoS (for finality/low energy). - -## Mechanism -- Blocks are mined (PoW) to provide entropy and sybil resistance. -- A committee of Stakers (PoS) must sign off on the block for finality. -- Substrate-based architecture. - -## Ghost Features -- Deterministic finality within 1 block. -- "Spectral" transactions that clear off-chain and settle via ZK-proofs. diff --git a/consensus.rs b/consensus.rs deleted file mode 100644 index 84eebd4..0000000 --- a/consensus.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Updated Ghost Chain Hybrid Consensus - PQC Hardened -use frame_support::pallet_prelude::*; -use sp_core::H256; - -#[pallet::config] -pub trait Config: frame_system::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - #[pallet::constant] - type Difficulty: Get; -} - -#[pallet::pallet] -pub struct Pallet(_); - -#[pallet::call] -impl Pallet { - #[pallet::weight(10_000)] - pub fn submit_pow_solution(origin: OriginFor, nonce: u64, puzzle_hash: T::Hash) -> DispatchResult { - let _who = ensure_signed(origin)?; - // Verify PoW nonce (Classical) - Ok(()) - } - - #[pallet::weight(50_000)] - pub fn finalize_with_pqc(origin: OriginFor, block_hash: T::Hash, signature: Vec) -> DispatchResult { - // Only PoS committee can call this - // Logic: Verify CRYSTALS-Dilithium signature before marking block as Final - Ok(()) - } -} diff --git a/docs/pqc-roadmap.md b/docs/pqc-roadmap.md new file mode 100644 index 0000000..e3f1a14 --- /dev/null +++ b/docs/pqc-roadmap.md @@ -0,0 +1,207 @@ +# Post-Quantum Crypto Integration Roadmap + +## Scope and current reality + +This document tracks the staged post-quantum integration for the Ghost blockchain. Stages that are +complete are marked **[DONE]**. Stages that remain are marked **[TODO]**. + +### What is implemented today + +- **Real PoW consensus**: `sc-consensus-pow` with double-Blake2-256 and on-chain difficulty retargeting. + Aura and GRANDPA have been removed from the node and runtime. +- **ML-DSA on-chain verification [DONE]**: ML-DSA-87 (Dilithium-5, NIST FIPS 204, security level 5) signature + verification runs inside the no\_std Wasm runtime via the pure-Rust `fips204` crate + (`pallets/pallet-ghost-consensus/src/pq_verify.rs`). Validators register ML-DSA public keys; + `validate_block` enforces ML-DSA signature checks when a key is registered; the general + `verify_pq_signature` extrinsic is available. Tested with real keygen/sign/verify and tamper tests. +- **ML-KEM-1024 node-side encryption [DONE]**: ML-KEM-1024 key encapsulation (NIST FIPS 203, security level 5) + + ChaCha20-Poly1305 AEAD is implemented for application-layer payload encryption and operator tooling + (`node/src/pq_encrypt.rs`, `fips203` + `chacha20poly1305` crates). + +### What is NOT post-quantum + +- **Node-to-node transport is classical.** libp2p uses a Noise/X25519 handshake. The `stable2407` + polkadot-sdk does not include a PQ-Noise variant. Network transport is not post-quantum. +- **Account signature scheme** still uses `MultiSignature` (sr25519/ed25519/ecdsa). ML-DSA is an + additional registered validator/attestation path, not a replacement for account signing. +- **No external audit** has been performed; the implementation is tested and functional but not + production-certified. + +## Standards baseline + +All implemented and planned work uses standardized primitives: + +- `ML-DSA` (NIST FIPS 204, August 13 2024) — post-quantum digital signatures; **implemented on-chain** +- `ML-KEM` (NIST FIPS 203, August 13 2024) — key encapsulation; **implemented node-side** +- `SLH-DSA` (NIST FIPS 205, August 13 2024) — conservative hash-based signature fallback; **not yet implemented** + +## Staged implementation plan + +### Phase A: PQ validator signatures and payload encryption — [DONE] + +- ML-DSA-87 on-chain verification in the Wasm runtime. +- Validator ML-DSA key registration extrinsic. +- `validate_block` enforces ML-DSA signature when a key is registered. +- ML-KEM-1024 + ChaCha20-Poly1305 node-side payload encryption. +- Real sign/verify/tamper test coverage. + +What this phase does NOT provide: PQ account transaction signing; PQ transport; BFT PQ finality. + +### Phase B: Hybrid validator identity and account PQ signing — [TODO] + +- Extend or replace `MultiSignature` in `runtime/src/lib.rs` to support PQ-signed extrinsics. +- Add SCALE encoding, extrinsic verification, benchmarks, and weight updates for PQ signatures. +- Prove key lifecycle: generation, rotation, revocation, and recovery for ML-DSA validator keys. +- Add monitoring for key mismatch, stale attestations, and signer failure. +- Benchmark: extrinsic size limits, block weight, transaction pool memory, and RPC payload sizes. + +Exit criteria: PQ-signed extrinsics execute in tests; malformed signatures fail deterministically; +block size and throughput impact are measured and documented. + +### Phase C: PQ transport — [TODO] + +- Replace libp2p Noise/X25519 with a PQ-Noise variant (requires polkadot-sdk / libp2p upgrade or + custom transport integration). +- Interoperability tests with classical peers during any transition period. +- Threat model covering downgrade paths, handshake failures, and replay behavior. + +Exit criteria: node-to-node connections are post-quantum hardened; classical fallback is either +disabled or explicitly controlled and documented. + +### Phase D: External audit and release gating — [TODO] + +- Internal cryptography review. +- Independent external cryptography audit covering ML-DSA and ML-KEM usage, runtime verification + boundaries, and key management. +- Independent protocol/integration audit. +- Fuzzing and property testing for signature parsing and verification boundaries. +- Performance and DoS review for oversized payloads and verification storms. +- Release checklist: rollback procedures, kill-switches, chain-upgrade safety. + +Exit criteria: all audit findings triaged and closed or explicitly accepted; production release notes +contain no unsupported PQ claims. + +## Concrete implementation roadmap for this repo + +### Stage 0: Threat model and success criteria — [DONE for implemented scope] + +Completed: + +- ML-DSA chosen as the primary PQ signature family (FIPS 204); ML-KEM-1024 for key encapsulation (FIPS 203). +- Target scope defined: validator/attestation signatures + application-layer payload encryption; account + transaction signing and transport are explicitly out of scope for the current phase. +- Runtime Wasm build compatibility confirmed (`fips204` in `no_std`). + +Remaining: + +- Written threat model document for Phase C transport work. +- Approved rollout and rollback criteria for Phase B account signing changes. + +### Stage 1: Crypto abstraction layer — [DONE] + +Completed: + +- `pq_verify.rs` internal module isolates all `fips204` API usage behind `verify_ml_dsa` / `validate_ml_dsa_pk`. +- Known-answer tests (real keygen/sign/verify) for ML-DSA-87 pass. +- `no_std` and Wasm build compatibility confirmed. +- Deterministic, panic-free verification with no RNG dependency in the runtime code path. + +Remaining: + +- Benchmark harness for ML-DSA verification cost at block limits. +- Equivalent abstraction module for Phase B account signing work. + +### Stage 2: PQ extrinsic support — [PARTIAL] + +Completed: + +- `verify_pq_signature` extrinsic verifies real ML-DSA signatures on-chain. +- `validate_block` enforces ML-DSA signature from the selected validator when a key is registered. +- Negative tests for invalid signatures are present. + +Remaining: + +- Extend or replace `MultiSignature` in `runtime/src/lib.rs` for general account PQ signing. +- Update transaction submission tooling and any tooling that assumes sr25519/ed25519/ecdsa. +- Benchmark verification cost; update weights and block limits accordingly. +- Add negative tests for malformed, truncated, replayed, and oversized signatures in the extrinsic path. + +### Stage 3: Tooling and operations — [PARTIAL] + +Completed: + +- Ghost CLI helpers for the miner and basic chain status. +- ML-KEM-1024 encryption utility in `node/src/pq_encrypt.rs`. + +Remaining: + +- Key generation/import/export tooling for ML-DSA scheme. +- Chain-spec and genesis support for PQ validator key material. +- RPC compatibility tests for author submission and account queries with PQ keys. +- Operator runbooks for rotation, backup, recovery, and incident response. + +### Stage 4: Hybrid validator migration — [PARTIAL] + +Completed: + +- Validator ML-DSA key registration and enforcement in `validate_block` is the functional core of hybrid + operation: validators can operate with both a classical account key and a registered ML-DSA key. + +Remaining: + +- Monitoring for key mismatch, stale attestations, and signer failure. +- Incident drills for lost or rotated ML-DSA keys. +- Operational evidence that dual-key validator workflows run reliably over extended periods. + +### Stage 5: PQ transport — [TODO] + +Primary code targets: + +- libp2p transport configuration (requires polkadot-sdk upgrade or custom integration) +- `node/src/service.rs` network configuration hooks + +Exit criteria: live block production no longer requires a classical Noise/X25519 handshake; +validator-node interoperability tests pass across restart, equivocation, and partition scenarios. + +### Stage 6: Audit and release gating — [TODO] + +See Phase D above. + +## Audit checklist + +An audit firm should be able to answer "yes" to all of these before any production claim: + +- Are all validator signature checks enforced using the new ML-DSA design, not only pallet-local messages? +- Are transaction signatures (if extended to account scheme) verified by standardized PQ algorithms? +- Are all runtime verifiers deterministic under Wasm? +- Are signature parsing and length checks hardened against panic and memory abuse? +- Are weight limits and block limits updated for worst-case PQ verification load? +- Are keystore, RPC, and validator workflows covered end to end? +- Are downgrade paths to classical-only behavior either disabled or explicitly controlled? +- Are wallet and operator UX paths resistant to accidental misuse? +- Are all public docs accurate about what remains classical? + +## What this repo can honestly claim at each milestone + +- **Now (Stage 1 + Stage 2 partial + Stage 4 partial)**: "the runtime verifies ML-DSA validator signatures + in the Wasm runtime; ML-KEM-1024 payload encryption is available node-side; ordinary account transactions + and network transport remain classical." +- After Stage 2 complete: "the runtime can verify PQ-signed extrinsics for all account types in tests." +- After Stage 4 complete: "validators operate hybrid classical plus ML-DSA identity workflows with + production key lifecycle tooling." +- After Stage 5 and Stage 6: "live consensus signatures, transaction signing, and node-to-node transport + have completed the PQ migration." + +## Documentation gate for future claims + +Do not update repository docs to claim transport-level or full-consensus PQ unless the relevant stage is +complete: + +- ML-DSA validator signature claims: Stage 1 + Stage 2 partial — **met for the validator path**. +- ML-KEM payload encryption claims: Stage 1 — **met for node-side use**. +- PQ account transaction-signing claims: require Stage 2 complete plus measured verification costs. +- PQ transport claims: require Stage 5 complete plus transport design, dependency review, + interoperability tests, and audit coverage. +- Production-ready claims: require Stage 6 complete. + +Describe any work outside these completed stages as in-progress or planned. diff --git a/docs/rust-setup.md b/docs/rust-setup.md index 00089ab..1c2eab7 100644 --- a/docs/rust-setup.md +++ b/docs/rust-setup.md @@ -2,6 +2,36 @@ This guide is for reference only, please check the latest information on getting started with Substrate [here](https://docs.substrate.io/main-docs/install/). +## Ghost local prerequisites + +The repository currently builds a Substrate node plus the experimental `pallet-ghost-consensus` runtime model. It does +not ship post-quantum cryptography or custom network encryption today, but any local exploration of future PQ or +network-crypto work still depends on a working native Rust, Wasm, and OpenSSL toolchain. + +Before you start, make sure your machine can provide: + +- `rustup`, `cargo`, and a stable Rust toolchain +- the `wasm32-unknown-unknown` target for runtime builds +- C/C++ build tooling required by Rust native dependencies +- OpenSSL headers and libraries +- Perl on native Windows, because `openssl-sys` may fall back to building vendored OpenSSL from source + +If you are setting up a fresh workstation for this repository, validate the environment with: + +```bash +cargo build --bin ghost-node +cargo test -p pallet-ghost-consensus +``` + +If your shell does not already pick up the repository helper toolchain, you can also install the components mirrored in +`env-setup/rust-toolchain.toml`: + +```bash +rustup default stable +rustup component add clippy rust-analyzer rust-src rustfmt +rustup target add wasm32-unknown-unknown +``` + This page will guide you through the **2 steps** needed to prepare a computer for **Substrate** development. Since Substrate is built with [the Rust programming language](https://www.rust-lang.org/), the first thing you will need to do is prepare the computer for Rust development - these steps will vary based on the computer's operating system. Once Rust @@ -74,6 +104,30 @@ recommended to use [Windows Subsystem Linux](https://docs.microsoft.com/en-us/wi Please refer to the separate [guide for native Windows development](https://docs.substrate.io/main-docs/install/windows/). +For this repository, WSL remains the preferred path for any work that touches network-facing cryptography, TLS, or +future PQ experiments. A native Windows shell can hit dependency builds that are harder to recover from. + +### Native Windows OpenSSL and Perl note + +An observed local blocker in this repository is: + +- `cargo check --bin ghost-node` can fail because `openssl-sys` needs `perl` while building vendored OpenSSL + +If you must stay on native Windows, install these pieces before debugging Rust errors: + +- Visual Studio Build Tools with the C++ workload +- Strawberry Perl or another Perl distribution available on `PATH` +- OpenSSL development libraries, or enough tooling for vendored OpenSSL builds + +After installing them, open a fresh shell and rerun: + +```powershell +cargo build --bin ghost-node +``` + +If you only need a reliable local environment for build, test, or audit prep, moving the repository into WSL is usually +faster than chasing native Windows OpenSSL toolchain issues. + ## Rust developer environment This guide uses installer and the `rustup` tool to manage the Rust toolchain. First install and @@ -100,6 +154,25 @@ rustup target add wasm32-unknown-unknown --toolchain nightly Now the best way to ensure that you have successfully prepared a computer for Substrate development is to follow the steps in [our first Substrate tutorial](https://docs.substrate.io/tutorials/v3/create-your-first-substrate-chain/). +For this repository specifically, also run: + +```bash +cargo build --bin ghost-node +cargo test -p pallet-ghost-consensus +``` + +If you are preparing for crypto-adjacent design or audit work, capture the output of: + +```bash +rustup show +cargo --version +rustc --version +perl -v +openssl version +``` + +That baseline is useful when reproducing dependency or toolchain failures across platforms. + ## Troubleshooting Substrate builds Sometimes you can't get the Substrate node template to compile out of the box. Here are some tips to help you work diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..f52ecb3 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,103 @@ +# Security Notes + +## Scope + +This document describes the current security posture of the Ghost blockchain and the minimum requirements for any +future work on the cryptographic stack. + +### What is implemented + +- Real Proof-of-Work block authoring via `sc-consensus-pow` (double-Blake2-256, `U256` difficulty). Aura and GRANDPA + have been removed. Finality is probabilistic longest-chain PoW, not BFT. +- On-chain ML-DSA signature verification (NIST FIPS 204, pure-Rust `fips204` crate, `no_std` Wasm runtime). + Validators may register ML-DSA public keys; `validate_block` enforces ML-DSA signature checks when a key is + registered. All three ML-DSA parameter sets (ML-DSA-44, ML-DSA-65, ML-DSA-87) are supported. +- Node-side ML-KEM-1024 key encapsulation + ChaCha20-Poly1305 AEAD (NIST FIPS 203) for application-layer payload + encryption (`node/src/pq_encrypt.rs`). + +### What is NOT post-quantum + +- **Node-to-node transport is classical.** The node uses libp2p with a Noise/X25519 handshake. The `stable2407` + polkadot-sdk does not include a PQ-Noise variant. Network transport is not post-quantum. +- **Ordinary account signatures use `MultiSignature`** (sr25519/ed25519/ecdsa). ML-DSA is an additional + registered validator/attestation path, not a replacement for the account signature scheme. + +### What has not been audited + +No external security audit of this codebase has been performed. The implementation is tested and functional, +but an audit and multi-node adversarial testing remain before any production mainnet claim is appropriate. + +## Local prerequisite checklist + +Before starting crypto-adjacent work, confirm that you can run the normal local build and test path: + +```bash +cargo build --bin ghost-node +cargo test -p pallet-ghost-consensus +``` + +You should also record the active toolchain and crypto-related host dependencies: + +```bash +rustup show +rustc --version +cargo --version +openssl version +perl -v +``` + +On native Windows, `perl -v` matters because this repository has already hit an `openssl-sys` failure where vendored +OpenSSL required Perl to continue building. + +## Windows-specific caution + +WSL is the preferred development environment for this codebase when working near TLS, libssl, or PQ dependency +evaluation. + +If native Windows is unavoidable, treat the following as mandatory baseline dependencies: + +- Visual Studio Build Tools with C++ +- a Perl distribution on `PATH` +- OpenSSL headers and libraries, or a working vendored OpenSSL build path + +Do not treat a successful pallet unit test run as proof that the node's native crypto dependencies are healthy. +The full node build should pass too. + +## Audit requirements for PQ or network crypto changes + +No PQ or network-crypto implementation should be described as complete, secure, or production-ready until it has +passed all of the following: + +1. A written threat model covering peer transport, key material, downgrade paths, replay behavior, and failure modes. +2. A dependency review for every new cryptographic crate, C library, or FFI boundary. +3. Reproducible local builds on the supported developer platforms, including at least one Unix-like environment. +4. Negative tests for malformed inputs, handshake failures, and algorithm-mismatch scenarios. +5. Secret-handling review for key generation, storage, rotation, logging, and crash output. +6. An external security audit by reviewers with cryptography and systems experience before any production claim. + +## Minimum evidence expected in a change + +Any serious PQ or network-crypto pull request should include: + +- the exact algorithms and libraries being evaluated +- why those choices fit the threat model +- platform-specific build notes, especially for Windows and OpenSSL-linked dependencies +- test coverage for success and failure paths +- a clear statement of what remains experimental or unaudited + +## Claim gates + +Use the following minimum gates before updating documentation language: + +- "PQ validator signatures" — ML-DSA verification is implemented and tested in the runtime. This gate is met for + the registered-validator path. It does not extend to the account `MultiSignature` scheme. +- "PQ payload encryption" — ML-KEM-1024 + ChaCha20-Poly1305 is implemented in `node/src/pq_encrypt.rs` for + application-layer use. This gate is met for operator/payload use cases. +- "PQ transport" — not met. Requires a PQ-Noise libp2p variant, an implemented transport design, + interoperability testing, and audit coverage for the actual network path. +- "PQ consensus" — not met. Finality is probabilistic PoW. Replacing Aura/GRANDPA with classical PoW does not + make consensus post-quantum; PQ consensus would require that all critical consensus signatures use PQ schemes. +- "Production ready" — not met until an external audit is complete, multi-node adversarial testing passes, and + all audit findings are triaged and resolved. + +If a gate is not met, do not describe the feature as complete for that scope. diff --git a/node/Cargo.toml b/node/Cargo.toml index 097c6f8..9bbcde6 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-node" -description = "Ghost blockchain node - Hybrid PoW + PoS consensus for fast, secure, and energy-efficient blockchain." +description = "Ghost blockchain node with an experimental Ghost consensus pallet." version = "0.0.0" license = "Unlicense" authors = ["Ghost Blockchain Team"] @@ -19,8 +19,18 @@ workspace = true [dependencies] clap = { workspace = true, features = ["derive"] } +codec = { workspace = true, default-features = true, features = ["derive"] } +# Post-quantum encryption (node-side): ML-KEM-1024 (FIPS 203) + ChaCha20-Poly1305 AEAD. +fips203 = { version = "0.4.3", default-features = false, features = ["ml-kem-1024", "default-rng"] } +chacha20poly1305 = { version = "0.10" } +# ML-DSA-87 (FIPS 204) keygen for the CLI `pq-keygen` helper (native binary only; the +# no_std Wasm runtime builds fips204 separately, without `default-rng`). +fips204 = { version = "0.4.6", default-features = false, features = ["ml-dsa-87", "default-rng"] } futures = { workspace = true, features = ["thread-pool"] } -jsonrpsee = { workspace = true, features = ["server"] } +futures-timer = "3.0.3" +jsonrpsee = { workspace = true, features = ["server", "http-client"] } +# Async runtime for the CLI wallet's RPC client (the node itself uses sc-cli's runtime). +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # substrate client sc-basic-authorship = { workspace = true, default-features = true } @@ -29,14 +39,17 @@ sc-client-api = { workspace = true, default-features = true } sc-consensus = { workspace = true, default-features = true } sc-consensus-aura = { workspace = true, default-features = true } sc-consensus-grandpa = { workspace = true, default-features = true } +sc-consensus-pow = { workspace = true, default-features = true } sc-executor = { workspace = true, default-features = true } sc-network = { workspace = true, default-features = true } sc-offchain = { workspace = true, default-features = true } +sc-rpc-api = { workspace = true, default-features = true } sc-service = { workspace = true, default-features = true } sc-telemetry = { workspace = true, default-features = true } sc-transaction-pool = { workspace = true, default-features = true } sc-transaction-pool-api = { workspace = true, default-features = true } sp-consensus-aura = { workspace = true, default-features = true } +sp-consensus-pow = { workspace = true, default-features = true } sp-core = { workspace = true, default-features = true } sp-genesis-builder = { workspace = true, default-features = true } @@ -53,6 +66,8 @@ sp-timestamp = { workspace = true, default-features = true } # frame and pallets frame-metadata-hash-extension = { workspace = true, default-features = true } frame-system = { workspace = true, default-features = true } +pallet-balances = { workspace = true, default-features = true } +pallet-ghost-consensus = { workspace = true, default-features = true } pallet-transaction-payment = { workspace = true, default-features = true } pallet-transaction-payment-rpc = { workspace = true, default-features = true } substrate-frame-rpc-system = { workspace = true, default-features = true } diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 61f1a76..4de312f 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -19,149 +19,156 @@ use std::{sync::Arc, time::Duration}; /// /// Note: Should only be used for benchmarking. pub struct RemarkBuilder { - client: Arc, + client: Arc, } impl RemarkBuilder { - /// Creates a new [`Self`] from the given client. - pub fn new(client: Arc) -> Self { - Self { client } - } + /// Creates a new [`Self`] from the given client. + pub fn new(client: Arc) -> Self { + Self { client } + } } impl frame_benchmarking_cli::ExtrinsicBuilder for RemarkBuilder { - fn pallet(&self) -> &str { - "system" - } - - fn extrinsic(&self) -> &str { - "remark" - } - - fn build(&self, nonce: u32) -> std::result::Result { - let acc = Sr25519Keyring::Bob.pair(); - let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( - self.client.as_ref(), - acc, - SystemCall::remark { remark: vec![] }.into(), - nonce, - ) - .into(); - - Ok(extrinsic) - } + fn pallet(&self) -> &str { + "system" + } + + fn extrinsic(&self) -> &str { + "remark" + } + + fn build(&self, nonce: u32) -> std::result::Result { + let acc = Sr25519Keyring::Bob.pair(); + let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( + self.client.as_ref(), + acc, + SystemCall::remark { remark: vec![] }.into(), + nonce, + ) + .into(); + + Ok(extrinsic) + } } /// Generates `Balances::TransferKeepAlive` extrinsics for the benchmarks. /// /// Note: Should only be used for benchmarking. pub struct TransferKeepAliveBuilder { - client: Arc, - dest: AccountId, - value: Balance, + client: Arc, + dest: AccountId, + value: Balance, } impl TransferKeepAliveBuilder { - /// Creates a new [`Self`] from the given client. - pub fn new(client: Arc, dest: AccountId, value: Balance) -> Self { - Self { client, dest, value } - } + /// Creates a new [`Self`] from the given client. + pub fn new(client: Arc, dest: AccountId, value: Balance) -> Self { + Self { + client, + dest, + value, + } + } } impl frame_benchmarking_cli::ExtrinsicBuilder for TransferKeepAliveBuilder { - fn pallet(&self) -> &str { - "balances" - } - - fn extrinsic(&self) -> &str { - "transfer_keep_alive" - } - - fn build(&self, nonce: u32) -> std::result::Result { - let acc = Sr25519Keyring::Bob.pair(); - let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( - self.client.as_ref(), - acc, - BalancesCall::transfer_keep_alive { dest: self.dest.clone().into(), value: self.value } - .into(), - nonce, - ) - .into(); - - Ok(extrinsic) - } + fn pallet(&self) -> &str { + "balances" + } + + fn extrinsic(&self) -> &str { + "transfer_keep_alive" + } + + fn build(&self, nonce: u32) -> std::result::Result { + let acc = Sr25519Keyring::Bob.pair(); + let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( + self.client.as_ref(), + acc, + BalancesCall::transfer_keep_alive { + dest: self.dest.clone().into(), + value: self.value, + } + .into(), + nonce, + ) + .into(); + + Ok(extrinsic) + } } /// Create a transaction using the given `call`. /// /// Note: Should only be used for benchmarking. pub fn create_benchmark_extrinsic( - client: &FullClient, - sender: sp_core::sr25519::Pair, - call: runtime::RuntimeCall, - nonce: u32, + client: &FullClient, + sender: sp_core::sr25519::Pair, + call: runtime::RuntimeCall, + nonce: u32, ) -> runtime::UncheckedExtrinsic { - let genesis_hash = client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"); - let best_hash = client.chain_info().best_hash; - let best_block = client.chain_info().best_number; - - let period = runtime::configs::BlockHashCount::get() - .checked_next_power_of_two() - .map(|c| c / 2) - .unwrap_or(2) as u64; - let tx_ext: runtime::TxExtension = ( - frame_system::AuthorizeCall::::new(), - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), - frame_system::CheckGenesis::::new(), - frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( - period, - best_block.saturated_into(), - )), - frame_system::CheckNonce::::from(nonce), - frame_system::CheckWeight::::new(), - pallet_transaction_payment::ChargeTransactionPayment::::from(0), - frame_metadata_hash_extension::CheckMetadataHash::::new(false), - frame_system::WeightReclaim::::new(), - ); - - let raw_payload = runtime::SignedPayload::from_raw( - call.clone(), - tx_ext.clone(), - ( - (), - (), - runtime::VERSION.spec_version, - runtime::VERSION.transaction_version, - genesis_hash, - best_hash, - (), - (), - (), - None, - (), - ), - ); - let signature = raw_payload.using_encoded(|e| sender.sign(e)); - - runtime::UncheckedExtrinsic::new_signed( - call, - sp_runtime::AccountId32::from(sender.public()).into(), - runtime::Signature::Sr25519(signature), - tx_ext, - ) + let genesis_hash = client + .block_hash(0) + .ok() + .flatten() + .expect("Genesis block exists; qed"); + let best_hash = client.chain_info().best_hash; + let best_block = client.chain_info().best_number; + + let period = runtime::configs::BlockHashCount::get() + .checked_next_power_of_two() + .map(|c| c / 2) + .unwrap_or(2) as u64; + let tx_ext: runtime::TxExtension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( + period, + best_block.saturated_into(), + )), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + ); + + let raw_payload = runtime::SignedPayload::from_raw( + call.clone(), + tx_ext.clone(), + ( + (), + runtime::VERSION.spec_version, + runtime::VERSION.transaction_version, + genesis_hash, + best_hash, + (), + (), + (), + None::<[u8; 32]>, + ), + ); + let signature = raw_payload.using_encoded(|e| sender.sign(e)); + + runtime::UncheckedExtrinsic::new_signed( + call, + sp_runtime::AccountId32::from(sender.public()).into(), + runtime::Signature::Sr25519(signature), + tx_ext, + ) } /// Generates inherent data for the `benchmark overhead` command. /// /// Note: Should only be used for benchmarking. pub fn inherent_benchmark_data() -> Result { - let mut inherent_data = InherentData::new(); - let d = Duration::from_millis(0); - let timestamp = sp_timestamp::InherentDataProvider::new(d.into()); + let mut inherent_data = InherentData::new(); + let d = Duration::from_millis(0); + let timestamp = sp_timestamp::InherentDataProvider::new(d.into()); - futures::executor::block_on(timestamp.provide_inherent_data(&mut inherent_data)) - .map_err(|e| format!("creating inherent data: {e:?}"))?; - Ok(inherent_data) + futures::executor::block_on(timestamp.provide_inherent_data(&mut inherent_data)) + .map_err(|e| format!("creating inherent data: {e:?}"))?; + Ok(inherent_data) } diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 3dbe406..3c71059 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -5,25 +5,30 @@ use solochain_template_runtime::WASM_BINARY; pub type ChainSpec = sc_service::GenericChainSpec; pub fn development_chain_spec() -> Result { - Ok(ChainSpec::builder( - WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, - None, - ) - .with_name("Ghost Development") - .with_id("ghost-dev") - .with_chain_type(ChainType::Development) - .with_genesis_config_preset_name(sp_genesis_builder::DEV_RUNTIME_PRESET) - .build()) + Ok(ChainSpec::builder( + WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, + None, + ) + .with_name("Ghost Development") + .with_id("ghost-dev") + .with_chain_type(ChainType::Development) + // Claimed PQ metadata registry data is not exposed here yet because the runtime preset only accepts + // fields that the pallet genesis APIs support. Extend the runtime preset first if the pallet + // gains a dedicated claimed-PQ-metadata genesis field. + .with_genesis_config_preset_name(solochain_template_runtime::genesis_config_presets::DEV_PRESET) + .build()) } pub fn local_chain_spec() -> Result { - Ok(ChainSpec::builder( - WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, - None, - ) - .with_name("Ghost Local Testnet") - .with_id("ghost-local") - .with_chain_type(ChainType::Local) - .with_genesis_config_preset_name(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET) - .build()) + Ok(ChainSpec::builder( + WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, + None, + ) + .with_name("Ghost Local Testnet") + .with_id("ghost-local") + .with_chain_type(ChainType::Local) + // Keep local/dev presets aligned: claimed PQ metadata seeding belongs in the runtime genesis payload + // once pallet-ghost-consensus exposes that surface, not in the chain spec wrapper itself. + .with_genesis_config_preset_name(solochain_template_runtime::genesis_config_presets::LOCAL_PRESET) + .build()) } diff --git a/node/src/cli.rs b/node/src/cli.rs index a9e33e1..f9cd384 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -1,126 +1,220 @@ #[derive(Debug, clap::Parser)] #[command( - name = "ghost-node", - about = "Ghost blockchain node - Hybrid PoW + PoS consensus", - version, - author + name = "ghost-node", + about = "Ghost node with classical transport, Proof-of-Work block authoring, longest-chain selection, and an experimental Ghost runtime pallet", + long_about = "Ghost node with classical transport (libp2p/litep2p), Proof-of-Work block authoring, longest-chain (PoW) finality, and an experimental Ghost runtime pallet.\n\nOn-chain ML-DSA-87 signature verification is active in the Ghost consensus pallet. An ML-KEM-1024 encryption module exists node-side. libp2p transport remains classical.\n\nAny PQ fields exposed by this CLI are record-only metadata fields. They are non-enforcing runtime records and opaque attestation envelopes for claim tracking, not live post-quantum transport, authoring, finality, or quantum encryption.", + version, + author )] pub struct Cli { - #[command(subcommand)] - pub subcommand: Option, + #[command(subcommand)] + pub subcommand: Option, - #[clap(flatten)] - pub run: sc_cli::RunCmd, + #[clap(flatten)] + pub run: sc_cli::RunCmd, } #[derive(Debug, clap::Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Subcommand { - /// Key management cli utilities - #[command(subcommand)] - Key(sc_cli::KeySubcommand), + /// Key management cli utilities + #[command(subcommand)] + Key(sc_cli::KeySubcommand), - /// Build a chain specification. - /// DEPRECATED: `build-spec` command will be removed after 1/04/2026. Use `export-chain-spec` - /// command instead. - #[deprecated( - note = "build-spec command will be removed after 1/04/2026. Use export-chain-spec command instead" - )] - BuildSpec(sc_cli::BuildSpecCmd), + /// Build a chain specification. + BuildSpec(sc_cli::BuildSpecCmd), - /// Export the chain specification. - ExportChainSpec(sc_cli::ExportChainSpecCmd), + /// Validate blocks. + CheckBlock(sc_cli::CheckBlockCmd), - /// Validate blocks. - CheckBlock(sc_cli::CheckBlockCmd), + /// Export blocks. + ExportBlocks(sc_cli::ExportBlocksCmd), - /// Export blocks. - ExportBlocks(sc_cli::ExportBlocksCmd), + /// Export the state of a given block into a chain spec. + ExportState(sc_cli::ExportStateCmd), - /// Export the state of a given block into a chain spec. - ExportState(sc_cli::ExportStateCmd), + /// Import blocks. + ImportBlocks(sc_cli::ImportBlocksCmd), - /// Import blocks. - ImportBlocks(sc_cli::ImportBlocksCmd), + /// Remove the whole chain. + PurgeChain(sc_cli::PurgeChainCmd), - /// Remove the whole chain. - PurgeChain(sc_cli::PurgeChainCmd), + /// Revert the chain to a previous state. + Revert(sc_cli::RevertCmd), - /// Revert the chain to a previous state. - Revert(sc_cli::RevertCmd), + /// Sub-commands concerned with benchmarking. + #[command(subcommand)] + Benchmark(frame_benchmarking_cli::BenchmarkCmd), - /// Sub-commands concerned with benchmarking. - #[command(subcommand)] - Benchmark(frame_benchmarking_cli::BenchmarkCmd), + /// Db meta columns information. + ChainInfo(sc_cli::ChainInfoCmd), - /// Db meta columns information. - ChainInfo(sc_cli::ChainInfoCmd), - - /// Ghost blockchain specific commands - #[command(subcommand)] - Ghost(GhostCommands), + /// Ghost blockchain specific commands + #[command(subcommand)] + Ghost(GhostCommands), } /// Ghost blockchain specific commands #[derive(Debug, clap::Subcommand)] pub enum GhostCommands { - /// Start mining blocks (PoW) - #[command(name = "mine")] - Mine { - /// Number of threads to use for mining - #[arg(long, default_value = "1")] - threads: usize, - - /// Mining difficulty target - #[arg(long)] - difficulty: Option, - }, - - /// Stake tokens for PoS validation - #[command(name = "stake")] - Stake { - /// Amount to stake (in Ghost tokens) - #[arg(long)] - amount: u128, - - /// Account to stake from (if not provided, uses default account) - #[arg(long)] - account: Option, - }, - - /// Unstake tokens - #[command(name = "unstake")] - Unstake { - /// Amount to unstake - #[arg(long)] - amount: u128, - - /// Account to unstake from - #[arg(long)] - account: Option, - }, - - /// Check balance and staking information - #[command(name = "balance")] - Balance { - /// Account to check (if not provided, shows all accounts) - #[arg(long)] - account: Option, - }, - - /// Show current consensus status - #[command(name = "status")] - Status { - /// Show detailed information - #[arg(long)] - detailed: bool, - }, - - /// Show validator information - #[command(name = "validators")] - Validators { - /// Show only active validators - #[arg(long)] - active_only: bool, - }, + /// Run the local PoW demo miner + #[command(name = "mine")] + Mine { + /// Number of CPU threads to use for the local demo miner + #[arg(long, default_value = "1", value_parser = clap::value_parser!(u16).range(1..))] + threads: u16, + + /// Conventional mining difficulty (numerically larger = harder, same convention + /// as the runtime). Defaults to 1_000_000. + #[arg(long)] + difficulty: Option, + }, + + /// Stake tokens for PoS validation (signs + submits to a running node) + #[command(name = "stake")] + Stake { + /// Amount to stake in raw runtime balance units + #[arg(long)] + amount: u128, + + /// Signer secret URI / dev seed (e.g. //Alice). Defaults to //Alice. + #[arg(long)] + account: Option, + + /// Node JSON-RPC endpoint. Defaults to http://127.0.0.1:9944. + #[arg(long)] + rpc_url: Option, + }, + + /// Unstake tokens (signs + submits to a running node) + #[command(name = "unstake")] + Unstake { + /// Amount to unstake in raw runtime balance units + #[arg(long)] + amount: u128, + + /// Signer secret URI / dev seed (e.g. //Alice). Defaults to //Alice. + #[arg(long)] + account: Option, + + /// Node JSON-RPC endpoint. Defaults to http://127.0.0.1:9944. + #[arg(long)] + rpc_url: Option, + }, + + /// Transfer balance to another account (signs + submits to a running node) + #[command(name = "transfer")] + Transfer { + /// Destination: an SS58 address or a dev seed (e.g. //Bob) + #[arg(long)] + to: String, + + /// Amount to transfer in raw runtime balance units + #[arg(long)] + amount: u128, + + /// Signer secret URI / dev seed (e.g. //Alice). Defaults to //Alice. + #[arg(long)] + account: Option, + + /// Node JSON-RPC endpoint. Defaults to http://127.0.0.1:9944. + #[arg(long)] + rpc_url: Option, + }, + + /// Query a live account balance from a running node + #[command(name = "balance")] + Balance { + /// Account: an SS58 address or a dev seed (e.g. //Alice). Defaults to //Alice. + #[arg(long)] + account: Option, + + /// Node JSON-RPC endpoint. Defaults to http://127.0.0.1:9944. + #[arg(long)] + rpc_url: Option, + }, + + /// Show live consensus and record-only PQ metadata status + #[command(name = "status")] + Status { + /// Show detailed information + #[arg(long)] + detailed: bool, + }, + + /// List the live staked validator set from a running node + #[command(name = "validators")] + Validators { + /// Reserved for future filtering; currently all staked validators are shown + #[arg(long)] + active_only: bool, + + /// Node JSON-RPC endpoint. Defaults to http://127.0.0.1:9944. + #[arg(long)] + rpc_url: Option, + }, + + /// Generate an ML-DSA-87 (FIPS 204) keypair to .pub and .sec + #[command(name = "pq-keygen")] + PqKeygen { + /// Output path prefix (writes .pub and .sec) + #[arg(long, default_value = "ghost-mldsa")] + out: String, + }, + + /// Register an ML-DSA-87 public key on-chain (signs + submits to a running node) + #[command(name = "register-key")] + RegisterKey { + /// Path to the 2592-byte ML-DSA-87 public key file (from `pq-keygen`) + #[arg(long)] + key: String, + + /// Signer secret URI / dev seed (e.g. //Alice). Defaults to //Alice. + #[arg(long)] + account: Option, + + /// Node JSON-RPC endpoint. Defaults to http://127.0.0.1:9944. + #[arg(long)] + rpc_url: Option, + }, + + /// Generate an ML-KEM-1024 (FIPS 203) encryption keypair to .ek and .dk + #[command(name = "pq-kem-keygen")] + PqKemKeygen { + /// Output path prefix (writes .ek public and .dk secret) + #[arg(long, default_value = "ghost-mlkem")] + out: String, + }, + + /// Encrypt a file to a recipient's ML-KEM-1024 public key (ML-KEM + ChaCha20-Poly1305) + #[command(name = "pq-encrypt")] + PqEncrypt { + /// Recipient encapsulation (public) key file (e.g. recipient.ek) + #[arg(long)] + to: String, + + /// Plaintext input file + #[arg(long = "in")] + input: String, + + /// Encrypted output file + #[arg(long = "out")] + output: String, + }, + + /// Decrypt a file with your ML-KEM-1024 decapsulation (secret) key + #[command(name = "pq-decrypt")] + PqDecrypt { + /// Your decapsulation (secret) key file (e.g. me.dk) + #[arg(long)] + dk: String, + + /// Encrypted input file + #[arg(long = "in")] + input: String, + + /// Decrypted output file + #[arg(long = "out")] + output: String, + }, } diff --git a/node/src/command.rs b/node/src/command.rs index 12f852a..ba2efb1 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -1,194 +1,213 @@ use crate::{ - benchmarking::{inherent_benchmark_data, RemarkBuilder, TransferKeepAliveBuilder}, - chain_spec, - cli::{Cli, GhostCommands, Subcommand}, - service, + benchmarking::{inherent_benchmark_data, RemarkBuilder, TransferKeepAliveBuilder}, + chain_spec, + cli::{Cli, GhostCommands, Subcommand}, + service, }; use frame_benchmarking_cli::{BenchmarkCmd, ExtrinsicFactory, SUBSTRATE_REFERENCE_HARDWARE}; use sc_cli::SubstrateCli; use sc_service::PartialComponents; -use solochain_template_runtime::{Block, EXISTENTIAL_DEPOSIT}; +use solochain_template_runtime::{Block, EXISTENTIAL_DEPOSIT, UNIT}; use sp_keyring::Sr25519Keyring; +const CLI_STATUS_SUMMARY: &str = + "Ghost node with classical transport, Proof-of-Work block authoring, longest-chain (PoW) finality, and an experimental Ghost runtime pallet."; +const PQ_READINESS_NOTE: &str = + "On-chain ML-DSA-87 signature verification is active in the Ghost consensus pallet. An ML-KEM-1024 encryption module exists node-side. libp2p transport remains classical. Any PQ fields are record-only metadata fields: non-enforcing runtime records plus opaque attestation envelopes for claim tracking."; + impl SubstrateCli for Cli { - fn impl_name() -> String { - "Ghost Node".into() - } + fn impl_name() -> String { + "Ghost Node".into() + } - fn impl_version() -> String { - env!("SUBSTRATE_CLI_IMPL_VERSION").into() - } + fn impl_version() -> String { + env!("SUBSTRATE_CLI_IMPL_VERSION").into() + } - fn description() -> String { - "Ghost blockchain node with hybrid PoW + PoS consensus".into() - } + fn description() -> String { + format!("{CLI_STATUS_SUMMARY} {PQ_READINESS_NOTE}") + } - fn author() -> String { - "Ghost Blockchain Team".into() - } + fn author() -> String { + "Ghost Blockchain Team".into() + } - fn support_url() -> String { - "https://github.com/CoolCreator247/ghost-blockchain".into() - } + fn support_url() -> String { + "https://github.com/devnull37/ghost-blockchain".into() + } - fn copyright_start_year() -> i32 { - 2025 - } + fn copyright_start_year() -> i32 { + 2025 + } - fn load_spec(&self, id: &str) -> Result, String> { - Ok(match id { - "dev" => Box::new(chain_spec::development_chain_spec()?), - "" | "local" => Box::new(chain_spec::local_chain_spec()?), - path => - Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?), - }) - } + fn load_spec(&self, id: &str) -> Result, String> { + Ok(match id { + "dev" => Box::new(chain_spec::development_chain_spec()?), + "" | "local" => Box::new(chain_spec::local_chain_spec()?), + path => Box::new(chain_spec::ChainSpec::from_json_file( + std::path::PathBuf::from(path), + )?), + }) + } } /// Parse and run command line arguments pub fn run() -> sc_cli::Result<()> { - let cli = Cli::from_args(); + let cli = Cli::from_args(); - match &cli.subcommand { - Some(Subcommand::Key(cmd)) => cmd.run(&cli), - #[allow(deprecated)] - Some(Subcommand::BuildSpec(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| cmd.run(config.chain_spec, config.network)) - }, - Some(Subcommand::CheckBlock(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, import_queue, .. } = - service::new_partial(&config)?; - Ok((cmd.run(client, import_queue), task_manager)) - }) - }, - Some(Subcommand::ExportChainSpec(cmd)) => { - let chain_spec = cli.load_spec(&cmd.chain)?; - cmd.run(chain_spec) - }, - Some(Subcommand::ExportBlocks(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?; - Ok((cmd.run(client, config.database), task_manager)) - }) - }, - Some(Subcommand::ExportState(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?; - Ok((cmd.run(client, config.chain_spec), task_manager)) - }) - }, - Some(Subcommand::ImportBlocks(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, import_queue, .. } = - service::new_partial(&config)?; - Ok((cmd.run(client, import_queue), task_manager)) - }) - }, - Some(Subcommand::PurgeChain(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| cmd.run(config.database)) - }, - Some(Subcommand::Revert(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, backend, .. } = - service::new_partial(&config)?; - let aux_revert = Box::new(|client, _, blocks| { - sc_consensus_grandpa::revert(client, blocks)?; - Ok(()) - }); - Ok((cmd.run(client, backend, Some(aux_revert)), task_manager)) - }) - }, - Some(Subcommand::Benchmark(cmd)) => { - let runner = cli.create_runner(cmd)?; + match &cli.subcommand { + Some(Subcommand::Key(cmd)) => cmd.run(&cli), + Some(Subcommand::BuildSpec(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| cmd.run(config.chain_spec, config.network)) + } + Some(Subcommand::CheckBlock(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + import_queue, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, import_queue), task_manager)) + }) + } + Some(Subcommand::ExportBlocks(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, config.database), task_manager)) + }) + } + Some(Subcommand::ExportState(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, config.chain_spec), task_manager)) + }) + } + Some(Subcommand::ImportBlocks(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + import_queue, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, import_queue), task_manager)) + }) + } + Some(Subcommand::PurgeChain(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| cmd.run(config.database)) + } + Some(Subcommand::Revert(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + backend, + .. + } = service::new_partial(&config)?; + // Proof-of-Work has no finality-gadget justifications to revert. + let aux_revert = Box::new(|_client, _, _blocks| Ok(())); + Ok((cmd.run(client, backend, Some(aux_revert)), task_manager)) + }) + } + Some(Subcommand::Benchmark(cmd)) => { + let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| { - // This switch needs to be in the client, since the client decides - // which sub-commands it wants to support. - match cmd { - BenchmarkCmd::Pallet(cmd) => { - if !cfg!(feature = "runtime-benchmarks") { - return Err( - "Runtime benchmarking wasn't enabled when building the node. \ + runner.sync_run(|config| { + // This switch needs to be in the client, since the client decides + // which sub-commands it wants to support. + match cmd { + BenchmarkCmd::Pallet(cmd) => { + if !cfg!(feature = "runtime-benchmarks") { + return Err( + "Runtime benchmarking wasn't enabled when building the node. \ You can enable it with `--features runtime-benchmarks`." - .into(), - ); - } + .into(), + ); + } - cmd.run_with_spec::, ()>(Some( - config.chain_spec, - )) - }, - BenchmarkCmd::Block(cmd) => { - let PartialComponents { client, .. } = service::new_partial(&config)?; - cmd.run(client) - }, - #[cfg(not(feature = "runtime-benchmarks"))] - BenchmarkCmd::Storage(_) => Err( - "Storage benchmarking can be enabled with `--features runtime-benchmarks`." - .into(), - ), - #[cfg(feature = "runtime-benchmarks")] - BenchmarkCmd::Storage(cmd) => { - let PartialComponents { client, backend, .. } = - service::new_partial(&config)?; - let db = backend.expose_db(); - let storage = backend.expose_storage(); - let shared_cache = backend.expose_shared_trie_cache(); + cmd.run_with_spec::, ()>(Some( + config.chain_spec, + )) + } + BenchmarkCmd::Block(cmd) => { + let PartialComponents { client, .. } = service::new_partial(&config)?; + cmd.run(client) + } + #[cfg(not(feature = "runtime-benchmarks"))] + BenchmarkCmd::Storage(_) => Err( + "Storage benchmarking can be enabled with `--features runtime-benchmarks`." + .into(), + ), + #[cfg(feature = "runtime-benchmarks")] + BenchmarkCmd::Storage(cmd) => { + let PartialComponents { + client, backend, .. + } = service::new_partial(&config)?; + let db = backend.expose_db(); + let storage = backend.expose_storage(); - cmd.run(config, client, db, storage, shared_cache) - }, - BenchmarkCmd::Overhead(cmd) => { - let PartialComponents { client, .. } = service::new_partial(&config)?; - let ext_builder = RemarkBuilder::new(client.clone()); + cmd.run(config, client, db, storage) + } + BenchmarkCmd::Overhead(cmd) => { + let PartialComponents { client, .. } = service::new_partial(&config)?; + let ext_builder = RemarkBuilder::new(client.clone()); - cmd.run( - config.chain_spec.name().into(), - client, - inherent_benchmark_data()?, - Vec::new(), - &ext_builder, - false, - ) - }, - BenchmarkCmd::Extrinsic(cmd) => { - let PartialComponents { client, .. } = service::new_partial(&config)?; - // Register the *Remark* and *TKA* builders. - let ext_factory = ExtrinsicFactory(vec![ - Box::new(RemarkBuilder::new(client.clone())), - Box::new(TransferKeepAliveBuilder::new( - client.clone(), - Sr25519Keyring::Alice.to_account_id(), - EXISTENTIAL_DEPOSIT, - )), - ]); + cmd.run( + config, + client, + inherent_benchmark_data()?, + Vec::new(), + &ext_builder, + ) + } + BenchmarkCmd::Extrinsic(cmd) => { + let PartialComponents { client, .. } = service::new_partial(&config)?; + // Register the *Remark* and *TKA* builders. + let ext_factory = ExtrinsicFactory(vec![ + Box::new(RemarkBuilder::new(client.clone())), + Box::new(TransferKeepAliveBuilder::new( + client.clone(), + Sr25519Keyring::Alice.to_account_id(), + EXISTENTIAL_DEPOSIT, + )), + ]); - cmd.run(client, inherent_benchmark_data()?, Vec::new(), &ext_factory) - }, - BenchmarkCmd::Machine(cmd) => - cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()), - } - }) - }, - Some(Subcommand::ChainInfo(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| cmd.run::(&config)) - }, - Some(Subcommand::Ghost(cmd)) => { - let runner = cli.create_runner(&cli.run)?; - runner.sync_run(|_config| handle_ghost_command(cmd)) - }, - None => { - let runner = cli.create_runner(&cli.run)?; - runner.run_node_until_exit(|config| async move { - match config.network.network_backend { + cmd.run(client, inherent_benchmark_data()?, Vec::new(), &ext_factory) + } + BenchmarkCmd::Machine(cmd) => { + cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()) + } + } + }) + } + Some(Subcommand::ChainInfo(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| cmd.run::(&config)) + } + Some(Subcommand::Ghost(cmd)) => { + let runner = cli.create_runner(&cli.run)?; + runner.sync_run(|_config| handle_ghost_command(cmd)) + } + None => { + let runner = cli.create_runner(&cli.run)?; + runner.run_node_until_exit(|config| async move { + match config.network.network_backend { sc_network::config::NetworkBackendType::Libp2p => service::new_full::< sc_network::NetworkWorker< solochain_template_runtime::opaque::Block, @@ -200,141 +219,167 @@ pub fn run() -> sc_cli::Result<()> { service::new_full::(config) .map_err(sc_cli::Error::Service), } - }) - }, - } + }) + } + } } /// Handle Ghost-specific CLI commands fn handle_ghost_command(cmd: &GhostCommands) -> sc_cli::Result<()> { - match cmd { - GhostCommands::Mine { threads, difficulty } => { - use crate::miner::{Miner, MiningBlockHeader}; - use sp_core::H256; - - let target_difficulty = difficulty.unwrap_or(u64::MAX / 1_000_000); + match cmd { + GhostCommands::Mine { + threads, + difficulty, + } => { + use crate::miner::{Miner, MiningBlockHeader}; + use sp_core::H256; - // Create a sample block header for mining demonstration - let block_header = MiningBlockHeader { - number: 1, - parent_hash: H256::zero(), - state_root: H256::from_low_u64_be(1), - extrinsics_root: H256::from_low_u64_be(2), - difficulty: target_difficulty, - }; + // Conventional difficulty (larger = harder), matching the runtime and + // crate::pow. The default samples a meaningful hash rate in ~a second. + let target_difficulty = difficulty.unwrap_or(1_000_000); + let block_header = MiningBlockHeader { + number: 1, + parent_hash: H256::zero(), + state_root: H256::from_low_u64_be(1), + extrinsics_root: H256::from_low_u64_be(2), + difficulty: target_difficulty, + }; + let miner = Miner::new(*threads as usize, target_difficulty) + .map_err(|message| sc_cli::Error::Input(message.into()))?; - let miner = Miner::new(*threads, target_difficulty); - - match miner.start(block_header) { - Some((nonce, stats)) => { - println!("\n📦 Block ready to submit:"); - println!(" Use this nonce: {}", nonce); - println!(" Submit to network using: ghost-node submit-block --nonce {}", nonce); - }, - None => { - println!("\n⚠️ Mining was interrupted or failed"); - } - } + match miner.start(block_header) { + Some((nonce, stats)) => { + println!("\nLocal PoW demo result"); + println!(" Nonce: {}", nonce); + println!(" Hashes computed: {}", stats.hashes_computed); + println!(" Hash rate: {:.2} H/s", stats.hash_rate); + println!(" Elapsed: {:.2}s", stats.elapsed_time.as_secs_f64()); + println!( + " This command does not submit a block or author a live chain block." + ); + } + None => { + println!("\nLocal PoW demo was interrupted or did not find a nonce."); + } + } - Ok(()) - }, - GhostCommands::Stake { amount, account } => { - println!("🔒 Staking tokens for PoS validation..."); - println!(" Amount: {} Ghost tokens", amount); - if let Some(acc) = account { - println!(" Account: {}", acc); - } else { - println!(" Using default account (Alice)"); - } - println!(" Minimum stake: 1 Ghost token"); - println!("\n📝 To stake tokens, submit this extrinsic:"); - println!(" ghostConsensus.stake({})", amount); - println!("\n💡 You can submit this via:"); - println!(" 1. Polkadot.js Apps UI (https://polkadot.js.org/apps)"); - println!(" 2. Using substrate-api-client"); - println!(" 3. Direct RPC call to your running node"); - Ok(()) - }, - GhostCommands::Unstake { amount, account } => { - println!("🔓 Unstaking tokens..."); - println!(" Amount: {} Ghost tokens", amount); - if let Some(acc) = account { - println!(" Account: {}", acc); - } else { - println!(" Using default account (Alice)"); - } - println!("\n📝 To unstake tokens, submit this extrinsic:"); - println!(" ghostConsensus.unstake({})", amount); - println!("\n💡 You can submit this via:"); - println!(" 1. Polkadot.js Apps UI (https://polkadot.js.org/apps)"); - println!(" 2. Using substrate-api-client"); - println!(" 3. Direct RPC call to your running node"); - Ok(()) - }, - GhostCommands::Balance { account } => { - println!("💰 Checking balance and staking information..."); - if let Some(acc) = account { - println!(" Account: {}", acc); - } else { - println!(" Showing default development accounts:"); - println!("\n Alice:"); - println!(" Address: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"); - println!(" Balance: 100 Ghost tokens (genesis)"); - println!("\n Bob:"); - println!(" Address: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"); - println!(" Balance: 100 Ghost tokens (genesis)"); - } - println!("\n💡 To check live balance, connect to your running node via:"); - println!(" Polkadot.js Apps UI: https://polkadot.js.org/apps/#/accounts"); - Ok(()) - }, - GhostCommands::Status { detailed } => { - println!("📊 Ghost Consensus Status"); - println!("═══════════════════════════════════════════════"); - println!(" Current Phase: PoW Mining → PoS Validation → Finalization"); - println!(" Block Time: 5 seconds"); - println!(" Consensus: Hybrid PoW + PoS"); - println!(" PoW Algorithm: Enhanced Blake2-256 (ASIC-resistant)"); - println!(" Reward Distribution: 40% miner, 60% stakers"); - println!(" Block Reward: 10 Ghost tokens per block"); + Ok(()) + } + GhostCommands::Stake { + amount, + account, + rpc_url, + } => { + let suri = account.as_deref().unwrap_or(crate::wallet::DEFAULT_SURI); + crate::wallet::stake(*amount, suri, rpc_url.as_deref()).map_err(sc_cli::Error::Input) + } + GhostCommands::Unstake { + amount, + account, + rpc_url, + } => { + let suri = account.as_deref().unwrap_or(crate::wallet::DEFAULT_SURI); + crate::wallet::unstake(*amount, suri, rpc_url.as_deref()).map_err(sc_cli::Error::Input) + } + GhostCommands::Transfer { + to, + amount, + account, + rpc_url, + } => { + let suri = account.as_deref().unwrap_or(crate::wallet::DEFAULT_SURI); + crate::wallet::transfer(to, *amount, suri, rpc_url.as_deref()) + .map_err(sc_cli::Error::Input) + } + GhostCommands::Balance { account, rpc_url } => { + let acc = account.as_deref().unwrap_or(crate::wallet::DEFAULT_SURI); + crate::wallet::show_balance(acc, rpc_url.as_deref()).map_err(sc_cli::Error::Input) + } + GhostCommands::Status { detailed } => { + println!("Ghost Consensus Status"); + println!("==============================================="); + println!(" Network transport: classical libp2p/litep2p stack"); + println!(" Live block authoring: Proof-of-Work (sc-consensus-pow)"); + println!(" Live finality: longest-chain (PoW) selection"); + println!(" Local CLI mining command: local PoW demo only"); + println!(" Experimental pallet model: PoW header submission plus stake-weighted validation"); + println!( + " PQ metadata registry: record-only, non-enforcing metadata fields and opaque attestation envelopes" + ); + println!(" Block Time: 5 seconds"); + println!(" Runtime pallet: ghostConsensus"); + println!(" Demo PoW algorithm: double Blake2-256"); + println!(" Pallet test reward split: 40% miner, 60% stakers"); + println!(" Pallet test block reward: {} raw units", 10 * UNIT); + println!(" On-chain PQ signatures: ML-DSA-87 signature verification active in Ghost consensus pallet"); + println!(" Node-side PQ encryption module: ML-KEM-1024 (libp2p transport remains classical)"); - if *detailed { - println!("\n📈 Detailed Information:"); - println!("═══════════════════════════════════════════════"); - println!(" Minimum Stake: 1 Ghost token"); - println!(" Slashing Conditions:"); - println!(" - Double Signing: 100% stake slash"); - println!(" - Invalid Block: 50% stake slash"); - println!(" - Downtime (>100 blocks): 10% stake slash"); - println!("\n Phase Flow:"); - println!(" 1. PoW Mining - Miners compete to find nonce"); - println!(" 2. PoS Validation - Validators sign blocks by stake weight"); - println!(" 3. Finalization - Rewards distributed, return to PoW"); - println!("\n Network Info:"); - println!(" Chain: Ghost Development Chain"); - println!(" Runtime: FRAME-based (Substrate)"); - println!(" Token: Ghost (GHTM)"); - } + if *detailed { + println!("\nDetailed Information:"); + println!("==============================================="); + println!(" Minimum Stake: {} raw units", UNIT); + println!(" Slashing Conditions:"); + println!(" - Double Signing: configured slash"); + println!(" - Invalid Block: configured slash"); + println!(" - Downtime (>100 blocks): 10% stake slash"); + println!("\n PQ Metadata Registry:"); + println!( + " - `ghostConsensus.register_pq_readiness(...)` stores record-only PQ metadata fields." + ); + println!( + " - `ghostConsensus.attest_pq_readiness(...)` stores opaque attestation envelopes only." + ); + println!( + " - `ghostConsensus.remove_pq_readiness()` clears that non-enforcing metadata record and its opaque attestations." + ); + println!( + " - These extrinsics do not activate live PQ transport, PQ block authoring, PQ finality, or quantum encryption." + ); + println!("\n Important Caveat:"); + println!(" - Networking remains on the standard classical transport stack."); + println!(" - Live blocks are authored via Proof-of-Work (sc-consensus-pow) and finalized by longest-chain selection."); + println!(" - `ghost mine` is a local demo and does not submit headers or author blocks."); + println!(" - The Ghost pallet is validated with tests and CLI guidance, not wired into node authoring."); + println!(" - PQ registry entries are non-enforcing metadata records, and attestations are opaque envelopes, not proof of live PQ enforcement."); + println!(" - ML-DSA-87 signature verification is active on-chain in the Ghost consensus pallet."); + println!(" - ML-KEM-1024 encryption module exists node-side; quantum key exchange and PQ session setup are not active on libp2p transport."); + println!("\n Pallet Flow:"); + println!(" 1. PoW Header Submission - a nonce is produced off-chain"); + println!(" 2. Stake-Weighted Validation - validators approve submitted work"); + println!(" 3. Finalization - pallet state advances after validation"); + println!("\n Network Info:"); + println!(" Chain: Ghost Development Chain"); + println!(" Runtime: FRAME-based (Substrate)"); + println!(" Balance unit: 1 Ghost = {} raw units", UNIT); + } - println!("\n💡 Connect your node to see live status via Polkadot.js Apps"); - Ok(()) - }, - GhostCommands::Validators { active_only } => { - println!("👥 Validator Information"); - println!("═══════════════════════════════════════════════"); - if *active_only { - println!(" Filter: Active validators only"); - } else { - println!(" Filter: All validators"); - } - println!("\n Default Genesis Validators:"); - println!(" - Alice (5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY)"); - println!(" - Bob (5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)"); - println!("\n💡 To see live validator info:"); - println!(" 1. Start your node: ./target/release/ghost-node --dev"); - println!(" 2. Connect via Polkadot.js Apps"); - println!(" 3. Navigate to Network → Staking"); - Ok(()) - }, - } + println!("\nConnect your node to inspect live state via Polkadot.js Apps."); + Ok(()) + } + GhostCommands::Validators { + active_only: _, + rpc_url, + } => crate::wallet::list_validators(rpc_url.as_deref()).map_err(sc_cli::Error::Input), + GhostCommands::PqKeygen { out } => { + crate::wallet::generate_ml_dsa_key(out).map_err(sc_cli::Error::Input) + } + GhostCommands::RegisterKey { + key, + account, + rpc_url, + } => { + let suri = account.as_deref().unwrap_or(crate::wallet::DEFAULT_SURI); + crate::wallet::register_ml_dsa_key(key, suri, rpc_url.as_deref()) + .map_err(sc_cli::Error::Input) + } + GhostCommands::PqKemKeygen { out } => { + crate::pq_encrypt::cli_keygen(out).map_err(sc_cli::Error::Input) + } + GhostCommands::PqEncrypt { to, input, output } => { + crate::pq_encrypt::cli_encrypt(to, input, output).map_err(sc_cli::Error::Input) + } + GhostCommands::PqDecrypt { dk, input, output } => { + crate::pq_encrypt::cli_decrypt(dk, input, output).map_err(sc_cli::Error::Input) + } + } } diff --git a/node/src/main.rs b/node/src/main.rs index 47cb494..d408c4b 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -6,8 +6,11 @@ mod chain_spec; mod cli; mod command; mod miner; +mod pow; +mod pq_encrypt; mod rpc; mod service; +mod wallet; fn main() -> sc_cli::Result<()> { command::run() diff --git a/node/src/miner.rs b/node/src/miner.rs index 76e92f2..694bae3 100644 --- a/node/src/miner.rs +++ b/node/src/miner.rs @@ -1,177 +1,199 @@ -//! Mining functionality for Ghost consensus - -use sp_core::H256; +//! Local CPU mining demo for the Ghost Proof-of-Work algorithm. +//! +//! This runs the EXACT work function the live node authors blocks with: +//! `crate::pow::pow_hash` (double Blake2-256 over `pre_hash || nonce`) checked against +//! the conventional `U256` difficulty by `crate::pow::meets_difficulty`. It is a +//! standalone benchmark/illustration that searches for a valid nonce over a synthetic +//! pre-hash and reports the hash rate. +//! +//! It does NOT submit blocks to a running node and does not author live chain blocks — +//! real authoring is driven by `sc-consensus-pow` in `service.rs`. Keeping the demo on +//! `crate::pow` means there is a single source of truth for the PoW algorithm. + +use sp_core::{H256, U256}; use sp_runtime::traits::{BlakeTwo256, Hash}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; +use crate::pow::{meets_difficulty, pow_hash}; + /// Mining statistics #[derive(Debug, Clone, Default)] pub struct MiningStats { - pub hashes_computed: u64, - pub blocks_found: u64, - pub hash_rate: f64, - pub elapsed_time: Duration, + pub hashes_computed: u64, + pub blocks_found: u64, + pub hash_rate: f64, + pub elapsed_time: Duration, } -/// Block header data for mining +/// Block header data for the mining demo. These fields are folded into a synthetic +/// pre-hash, mirroring how the node mines over a block's pre-seal hash. #[derive(Clone, Debug)] pub struct MiningBlockHeader { - pub number: u32, - pub parent_hash: H256, - pub state_root: H256, - pub extrinsics_root: H256, - pub difficulty: u64, + pub number: u32, + pub parent_hash: H256, + pub state_root: H256, + pub extrinsics_root: H256, + /// Conventional difficulty: numerically larger = harder (matches the runtime and + /// `crate::pow`). A nonce is valid iff `pow_hash <= U256::MAX / difficulty`. + pub difficulty: u64, } /// Miner instance pub struct Miner { - threads: usize, - target_difficulty: u64, - running: Arc, - hashes: Arc, + threads: usize, + difficulty: U256, + running: Arc, + hashes: Arc, } impl Miner { - /// Create a new miner - pub fn new(threads: usize, target_difficulty: u64) -> Self { - Self { - threads, - target_difficulty, - running: Arc::new(AtomicBool::new(false)), - hashes: Arc::new(AtomicU64::new(0)), - } - } - - /// Start mining - pub fn start(&self, block_header: MiningBlockHeader) -> Option<(u64, MiningStats)> { - println!("🚀 Starting Ghost PoW mining..."); - println!(" Block Number: {}", block_header.number); - println!(" Target Difficulty: {}", self.target_difficulty); - println!(" Mining Threads: {}", self.threads); - println!(" Algorithm: Enhanced Blake2-256 (ASIC-resistant)\n"); - - self.running.store(true, Ordering::SeqCst); - let start_time = Instant::now(); - - let found_nonce = Arc::new(AtomicU64::new(0)); - let found_solution = Arc::new(AtomicBool::new(false)); - - let mut handles = vec![]; - - // Spawn mining threads - for thread_id in 0..self.threads { - let running = Arc::clone(&self.running); - let hashes = Arc::clone(&self.hashes); - let found_nonce = Arc::clone(&found_nonce); - let found_solution = Arc::clone(&found_solution); - let header = block_header.clone(); - let target_difficulty = self.target_difficulty; - let threads = self.threads; - - let handle = thread::spawn(move || { - let mut nonce = thread_id as u64; - let step = threads as u64; - - while running.load(Ordering::SeqCst) && !found_solution.load(Ordering::SeqCst) { - // Enhanced Blake2 PoW (double hash for ASIC resistance) - let hash_input = ( - header.number, - header.parent_hash, - header.state_root, - header.extrinsics_root, - nonce, - ); - - let first_hash = BlakeTwo256::hash_of(&hash_input); - let final_hash = BlakeTwo256::hash_of(&first_hash); - let hash_value = u64::from_be_bytes( - final_hash.as_bytes()[0..8].try_into().unwrap_or_default(), - ); - - hashes.fetch_add(1, Ordering::Relaxed); - - // Check if solution found - if hash_value <= target_difficulty { - found_solution.store(true, Ordering::SeqCst); - found_nonce.store(nonce, Ordering::SeqCst); - running.store(false, Ordering::SeqCst); - break; - } - - nonce = nonce.wrapping_add(step); - - // Periodic check to allow graceful shutdown - if nonce % 100_000 == 0 { - thread::sleep(Duration::from_micros(1)); - } - } - }); - - handles.push(handle); - } - - // Wait for all threads to complete - for handle in handles { - let _ = handle.join(); - } - - let elapsed = start_time.elapsed(); - let total_hashes = self.hashes.load(Ordering::SeqCst); - let hash_rate = total_hashes as f64 / elapsed.as_secs_f64(); - - let stats = MiningStats { - hashes_computed: total_hashes, - blocks_found: if found_solution.load(Ordering::SeqCst) { 1 } else { 0 }, - hash_rate, - elapsed_time: elapsed, - }; - - if found_solution.load(Ordering::SeqCst) { - let nonce = found_nonce.load(Ordering::SeqCst); - println!("\n✅ Block mined successfully!"); - println!(" Nonce: {}", nonce); - println!(" Hashes computed: {}", total_hashes); - println!(" Hash rate: {:.2} H/s", hash_rate); - println!(" Time elapsed: {:.2}s", elapsed.as_secs_f64()); - Some((nonce, stats)) - } else { - println!("\n❌ Mining stopped without finding solution"); - println!(" Hashes computed: {}", total_hashes); - println!(" Time elapsed: {:.2}s", elapsed.as_secs_f64()); - None - } - } - - /// Stop mining - pub fn stop(&self) { - self.running.store(false, Ordering::SeqCst); - } + /// Create a new miner. `difficulty` uses the conventional convention (larger = harder). + pub fn new(threads: usize, difficulty: u64) -> Result { + if threads == 0 { + return Err("mining threads must be at least 1"); + } + + Ok(Self { + threads, + difficulty: U256::from(difficulty.max(1)), + running: Arc::new(AtomicBool::new(false)), + hashes: Arc::new(AtomicU64::new(0)), + }) + } + + /// Search for a nonce whose Ghost PoW hash satisfies the difficulty. + pub fn start(&self, block_header: MiningBlockHeader) -> Option<(u64, MiningStats)> { + println!("Starting local Ghost PoW demo mining..."); + println!(" Block Number: {}", block_header.number); + println!( + " Difficulty (conventional, larger = harder): {}", + block_header.difficulty + ); + println!(" Mining Threads: {}", self.threads); + println!(" Algorithm: double Blake2-256 over pre_hash || nonce (same as live authoring)"); + println!(" Note: this demo does not submit blocks to a running node.\n"); + + // Synthetic pre-hash from the header fields. The live node instead mines over the + // block's real pre-seal hash, but the per-nonce work function is identical. + let pre_hash = BlakeTwo256::hash_of(&( + block_header.number, + block_header.parent_hash, + block_header.state_root, + block_header.extrinsics_root, + )); + + self.hashes.store(0, Ordering::SeqCst); + self.running.store(true, Ordering::SeqCst); + let start_time = Instant::now(); + + let found_nonce = Arc::new(AtomicU64::new(0)); + let found_solution = Arc::new(AtomicBool::new(false)); + + let mut handles = vec![]; + + // Spawn mining threads; each searches a disjoint arithmetic subsequence of nonces. + for thread_id in 0..self.threads { + let running = Arc::clone(&self.running); + let hashes = Arc::clone(&self.hashes); + let found_nonce = Arc::clone(&found_nonce); + let found_solution = Arc::clone(&found_solution); + let difficulty = self.difficulty; + let threads = self.threads; + + let handle = thread::spawn(move || { + let mut nonce = thread_id as u64; + let step = threads as u64; + + while running.load(Ordering::SeqCst) && !found_solution.load(Ordering::SeqCst) { + hashes.fetch_add(1, Ordering::Relaxed); + + if meets_difficulty(&pow_hash(&pre_hash, nonce), difficulty) { + found_solution.store(true, Ordering::SeqCst); + found_nonce.store(nonce, Ordering::SeqCst); + running.store(false, Ordering::SeqCst); + break; + } + + nonce = nonce.wrapping_add(step); + } + }); + + handles.push(handle); + } + + // Wait for all threads to complete + for handle in handles { + let _ = handle.join(); + } + + let elapsed = start_time.elapsed(); + let total_hashes = self.hashes.load(Ordering::SeqCst); + let hash_rate = total_hashes as f64 / elapsed.as_secs_f64().max(f64::MIN_POSITIVE); + + let stats = MiningStats { + hashes_computed: total_hashes, + blocks_found: if found_solution.load(Ordering::SeqCst) { + 1 + } else { + 0 + }, + hash_rate, + elapsed_time: elapsed, + }; + + if found_solution.load(Ordering::SeqCst) { + let nonce = found_nonce.load(Ordering::SeqCst); + println!("\nLocal PoW demo found a valid nonce."); + println!(" Nonce: {}", nonce); + println!(" Hashes computed: {}", total_hashes); + println!(" Hash rate: {:.2} H/s", hash_rate); + println!(" Time elapsed: {:.2}s", elapsed.as_secs_f64()); + Some((nonce, stats)) + } else { + println!("\nMining stopped without finding a solution."); + println!(" Hashes computed: {}", total_hashes); + println!(" Time elapsed: {:.2}s", elapsed.as_secs_f64()); + None + } + } + + /// Stop mining + pub fn stop(&self) { + self.running.store(false, Ordering::SeqCst); + } } #[cfg(test)] mod tests { - use super::*; - - #[test] - fn test_mining_with_easy_difficulty() { - let header = MiningBlockHeader { - number: 1, - parent_hash: H256::zero(), - state_root: H256::from_low_u64_be(1), - extrinsics_root: H256::from_low_u64_be(2), - difficulty: u64::MAX / 1000, // Easy difficulty for testing - }; - - let miner = Miner::new(2, u64::MAX / 1000); - let result = miner.start(header); - - assert!(result.is_some()); - let (nonce, stats) = result.unwrap(); - assert!(nonce > 0 || nonce == 0); // Any nonce is valid - assert!(stats.hashes_computed > 0); - assert_eq!(stats.blocks_found, 1); - } + use super::*; + + #[test] + fn demo_mines_a_nonce_that_verifies_under_the_node_pow() { + let header = MiningBlockHeader { + number: 1, + parent_hash: H256::zero(), + state_root: H256::from_low_u64_be(1), + extrinsics_root: H256::from_low_u64_be(2), + difficulty: 8, // conventional: ~1 in 8 hashes pass -> found quickly + }; + let pre_hash = BlakeTwo256::hash_of(&( + header.number, + header.parent_hash, + header.state_root, + header.extrinsics_root, + )); + + let (nonce, stats) = Miner::new(2, 8) + .expect("valid non-zero thread count") + .start(header) + .expect("a valid nonce exists at low difficulty"); + + assert_eq!(stats.blocks_found, 1); + assert!(stats.hashes_computed > 0); + // The demo's result verifies under the SAME function the node checks seals with. + assert!(meets_difficulty(&pow_hash(&pre_hash, nonce), U256::from(8u64))); + } } diff --git a/node/src/pow.rs b/node/src/pow.rs new file mode 100644 index 0000000..4f930de --- /dev/null +++ b/node/src/pow.rs @@ -0,0 +1,133 @@ +//! Real Proof-of-Work algorithm for the Ghost node. +//! +//! Block authoring is driven by `sc-consensus-pow`. A miner searches for a `nonce` +//! such that the double-Blake2-256 hash of `(pre_hash || nonce)` satisfies the +//! current difficulty. Difficulty is read from the runtime via the standard +//! `sp_consensus_pow::DifficultyApi`, which exposes the value retargeted on-chain by +//! `pallet-ghost-consensus`. +//! +//! Difficulty uses the conventional convention: numerically larger = harder. A hash +//! (as a big-endian 256-bit integer) is valid iff it is `<= U256::MAX / difficulty`. +//! This makes difficulty a direct measure of work, which is exactly what +//! `sc-consensus-pow` sums for fork-choice (`TotalDifficulty`). + +use std::sync::Arc; + +use codec::{Decode, Encode}; +use sc_consensus_pow::{Error as PowError, PowAlgorithm}; +use sp_api::ProvideRuntimeApi; +use sp_consensus_pow::{DifficultyApi, Seal}; +use sp_core::{H256, U256}; +use sp_runtime::{ + generic::BlockId, + traits::{BlakeTwo256, Block as BlockT, Hash}, +}; + +use solochain_template_runtime::opaque::Block; + +/// The PoW seal: a single nonce. SCALE-encoded into the opaque `Seal` (`Vec`). +#[derive(Clone, Copy, PartialEq, Eq, Encode, Decode, Debug)] +pub struct GhostSeal { + pub nonce: u64, +} + +/// Double Blake2-256 work hash over `(pre_hash || nonce)`. +pub fn pow_hash(pre_hash: &H256, nonce: u64) -> H256 { + let mut data = [0u8; 40]; + data[..32].copy_from_slice(pre_hash.as_bytes()); + data[32..].copy_from_slice(&nonce.to_le_bytes()); + let first = BlakeTwo256::hash(&data); + BlakeTwo256::hash(first.as_bytes()) +} + +/// Whether `hash` satisfies `difficulty` (larger difficulty = harder). +pub fn meets_difficulty(hash: &H256, difficulty: U256) -> bool { + let work = U256::from_big_endian(hash.as_bytes()); + let target = U256::MAX / difficulty.max(U256::one()); + work <= target +} + +/// The Ghost PoW algorithm. Reads difficulty from the runtime each block. +pub struct GhostPow { + client: Arc, +} + +impl GhostPow { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +impl Clone for GhostPow { + fn clone(&self) -> Self { + Self { + client: self.client.clone(), + } + } +} + +impl PowAlgorithm for GhostPow +where + C: ProvideRuntimeApi + Send + Sync, + C::Api: DifficultyApi, +{ + type Difficulty = U256; + + fn difficulty(&self, parent: ::Hash) -> Result> { + self.client + .runtime_api() + .difficulty(parent) + .map_err(|err| PowError::Other(format!("difficulty runtime API failed: {err}"))) + } + + fn verify( + &self, + _parent: &BlockId, + pre_hash: &::Hash, + _pre_digest: Option<&[u8]>, + seal: &Seal, + difficulty: Self::Difficulty, + ) -> Result> { + let seal = match GhostSeal::decode(&mut &seal[..]) { + Ok(seal) => seal, + Err(_) => return Ok(false), + }; + Ok(meets_difficulty(&pow_hash(pre_hash, seal.nonce), difficulty)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn difficulty_one_accepts_any_hash() { + // difficulty = 1 => target = U256::MAX => every hash is valid. + assert!(meets_difficulty(&H256::repeat_byte(0xFF), U256::one())); + } + + #[test] + fn high_difficulty_rejects_most_hashes() { + // An all-0xFF hash is the maximum value, so any difficulty > 1 rejects it. + assert!(!meets_difficulty(&H256::repeat_byte(0xFF), U256::from(2u64))); + } + + #[test] + fn mining_finds_a_nonce_at_easy_difficulty() { + let pre_hash = H256::repeat_byte(0x42); + let difficulty = U256::from(8u64); // ~1 in 8 hashes pass + let mut nonce = 0u64; + let mut found = None; + for _ in 0..100_000 { + if meets_difficulty(&pow_hash(&pre_hash, nonce), difficulty) { + found = Some(nonce); + break; + } + nonce += 1; + } + let nonce = found.expect("a valid nonce exists at low difficulty"); + // The seal round-trips and verifies. + let seal = GhostSeal { nonce }; + assert!(meets_difficulty(&pow_hash(&pre_hash, seal.nonce), difficulty)); + } +} diff --git a/node/src/pq_encrypt.rs b/node/src/pq_encrypt.rs new file mode 100644 index 0000000..e47e893 --- /dev/null +++ b/node/src/pq_encrypt.rs @@ -0,0 +1,596 @@ +//! Real post-quantum encryption for the Ghost node — ML-KEM-1024 + ChaCha20-Poly1305. +//! +//! # What this module provides +//! +//! * **ML-KEM-1024 key encapsulation** (NIST FIPS 203, security category 5, the +//! "Kyber-1024" parameter set) via the [`fips203`] crate (integritychain, same +//! author/design as the `fips204` ML-DSA crate already used for signatures). +//! +//! * **Hybrid authenticated encryption** of arbitrary payloads: ML-KEM-1024 for +//! key agreement, ChaCha20-Poly1305 (RustCrypto, IETF RFC 8439) for the actual +//! AEAD seal/open. The ML-KEM shared secret (32 bytes) is used directly as the +//! AEAD key. +//! +//! # Security boundary — please read before deploying +//! +//! This module provides real ML-KEM-1024 post-quantum key encapsulation and +//! ChaCha20-Poly1305 authenticated encryption for payloads and operator tooling. +//! It does **NOT** replace the libp2p Noise/X25519 transport handshake: the +//! `stable2407` polkadot-sdk ships libp2p without a PQ-Noise variant, so +//! **node-to-node transport remains classical (X25519 + ChaCha20-Poly1305 Noise) +//! and is NOT yet post-quantum**. Use this module for application-layer messages, +//! key storage, and operator utilities — not as a claim that the network transport +//! is post-quantum hardened. +//! +//! # FIPS 203 byte-length reference (ML-KEM-1024) +//! +//! | Artifact | Constant | Bytes | +//! |------------------------|------------------|-------| +//! | Encapsulation key (EK) | [`EK_LEN`] | 1 568 | +//! | Decapsulation key (DK) | [`DK_LEN`] | 3 168 | +//! | Ciphertext (CT) | [`CT_LEN`] | 1 568 | +//! | Shared secret (SSK) | [`SSK_LEN`] | 32 | +//! | AEAD nonce | [`NONCE_LEN`] | 12 | + +use chacha20poly1305::{ + aead::{Aead, AeadCore, KeyInit, OsRng}, + ChaCha20Poly1305, Key, Nonce, +}; +use fips203::{ + ml_kem_1024, + traits::{Decaps, Encaps, KeyGen, SerDes}, +}; + +// ── ML-KEM-1024 length constants ───────────────────────────────────────────── + +/// Byte length of the ML-KEM-1024 encapsulation (public) key — 1 568 bytes. +pub const EK_LEN: usize = 1568; +/// Byte length of the ML-KEM-1024 decapsulation (secret) key — 3 168 bytes. +pub const DK_LEN: usize = 3168; +/// Byte length of the ML-KEM-1024 ciphertext — 1 568 bytes. +pub const CT_LEN: usize = 1568; +/// Byte length of the ML-KEM shared secret for all parameter sets — 32 bytes. +pub const SSK_LEN: usize = 32; +/// Byte length of the ChaCha20-Poly1305 nonce — 12 bytes. +pub const NONCE_LEN: usize = 12; + +// ── Error type ──────────────────────────────────────────────────────────────── + +/// Errors returned by the ML-KEM and hybrid-encryption helpers. +#[derive(Debug, PartialEq, Eq)] +pub enum PqEncryptError { + /// The provided byte slice does not have the expected length for this type. + /// + /// Carries `(expected, got)`. + BadLength(usize, usize), + /// The `fips203` crate rejected the key, ciphertext, or shared secret. + Fips203(&'static str), + /// ChaCha20-Poly1305 authentication tag verification failed, or the payload + /// is otherwise malformed. + AeadError, + /// Key generation failed (OS RNG unavailable or fips203 error). + KeyGenFailed, + /// Encapsulation failed (OS RNG unavailable or fips203 error). + EncapsFailed, +} + +impl core::fmt::Display for PqEncryptError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::BadLength(exp, got) => + write!(f, "bad byte length: expected {exp}, got {got}"), + Self::Fips203(msg) => write!(f, "fips203 error: {msg}"), + Self::AeadError => write!(f, "AEAD authentication/decryption failed"), + Self::KeyGenFailed => write!(f, "ML-KEM-1024 key generation failed"), + Self::EncapsFailed => write!(f, "ML-KEM-1024 encapsulation failed"), + } + } +} + +// ── Low-level ML-KEM-1024 wrappers ─────────────────────────────────────────── + +/// Generate an ML-KEM-1024 keypair using the OS random number generator. +/// +/// Returns `(ek_bytes, dk_bytes)` where: +/// * `ek_bytes` is the 1 568-byte **encapsulation** (public) key — share this. +/// * `dk_bytes` is the 3 168-byte **decapsulation** (secret) key — keep secret. +/// +/// Uses [`fips203::ml_kem_1024::KG::try_keygen`], which calls `getrandom` +/// internally. Never panics; propagates RNG/parameter errors as +/// [`PqEncryptError::KeyGenFailed`]. +pub fn keygen() -> Result<([u8; EK_LEN], [u8; DK_LEN]), PqEncryptError> { + let (ek, dk) = ml_kem_1024::KG::try_keygen().map_err(|_| PqEncryptError::KeyGenFailed)?; + Ok((ek.into_bytes(), dk.into_bytes())) +} + +/// Encapsulate to an ML-KEM-1024 encapsulation key. +/// +/// Takes the recipient's serialised encapsulation key (`ek_bytes`, must be +/// exactly [`EK_LEN`] = 1 568 bytes). Returns `(shared_secret, ciphertext)` +/// where: +/// * `shared_secret` is 32 bytes — used as the AEAD key on both sides. +/// * `ciphertext` is [`CT_LEN`] = 1 568 bytes — send to the decapsulating party. +/// +/// Returns an error if `ek_bytes` is the wrong length or is structurally invalid +/// according to FIPS 203. Never panics. +pub fn encapsulate( + ek_bytes: &[u8], +) -> Result<([u8; SSK_LEN], [u8; CT_LEN]), PqEncryptError> { + // Length-check before the fixed-array cast so the error is descriptive. + if ek_bytes.len() != EK_LEN { + return Err(PqEncryptError::BadLength(EK_LEN, ek_bytes.len())); + } + let ek_arr: [u8; EK_LEN] = ek_bytes.try_into().expect("length checked above"); + let ek = ml_kem_1024::EncapsKey::try_from_bytes(ek_arr) + .map_err(PqEncryptError::Fips203)?; + + let (ssk, ct) = ek.try_encaps().map_err(|_| PqEncryptError::EncapsFailed)?; + Ok((ssk.into_bytes(), ct.into_bytes())) +} + +/// Decapsulate an ML-KEM-1024 ciphertext using the holder's decapsulation key. +/// +/// * `dk_bytes` — the 3 168-byte **decapsulation** (secret) key. +/// * `ct_bytes` — the 1 568-byte ciphertext produced by [`encapsulate`]. +/// +/// Returns the 32-byte shared secret, which will match what the encapsulating +/// party received, **unless the ciphertext is malformed** — in that case FIPS 203 +/// mandates "implicit rejection": a different pseudo-random 32-byte value is +/// returned (so the caller never learns *whether* the ciphertext was valid; +/// authentication must be done at the AEAD layer). +pub fn decapsulate( + dk_bytes: &[u8], + ct_bytes: &[u8], +) -> Result<[u8; SSK_LEN], PqEncryptError> { + if dk_bytes.len() != DK_LEN { + return Err(PqEncryptError::BadLength(DK_LEN, dk_bytes.len())); + } + if ct_bytes.len() != CT_LEN { + return Err(PqEncryptError::BadLength(CT_LEN, ct_bytes.len())); + } + let dk_arr: [u8; DK_LEN] = dk_bytes.try_into().expect("length checked above"); + let ct_arr: [u8; CT_LEN] = ct_bytes.try_into().expect("length checked above"); + + let dk = ml_kem_1024::DecapsKey::try_from_bytes(dk_arr) + .map_err(PqEncryptError::Fips203)?; + let ct = ml_kem_1024::CipherText::try_from_bytes(ct_arr) + .map_err(PqEncryptError::Fips203)?; + + let ssk = dk.try_decaps(&ct).map_err(PqEncryptError::Fips203)?; + Ok(ssk.into_bytes()) +} + +// ── Hybrid message type ─────────────────────────────────────────────────────── + +/// A fully self-contained post-quantum encrypted message. +/// +/// To send a message to a recipient, you only need their 1 568-byte +/// encapsulation key. All the pieces needed to decrypt are bundled here. +/// +/// Wire format (no external framing required for single-message use): +/// ```text +/// kem_ciphertext : [u8; 1568] // ML-KEM-1024 ciphertext +/// nonce : [u8; 12] // ChaCha20-Poly1305 nonce (random) +/// aead_ciphertext : Vec // plaintext + 16-byte Poly1305 tag +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncryptedMessage { + /// ML-KEM-1024 ciphertext that carries the encapsulated shared secret. + pub kem_ciphertext: [u8; CT_LEN], + /// Random 12-byte nonce for ChaCha20-Poly1305. + pub nonce: [u8; NONCE_LEN], + /// ChaCha20-Poly1305 sealed payload (plaintext length + 16-byte auth tag). + pub aead_ciphertext: Vec, +} + +impl EncryptedMessage { + /// Serialise to a flat `Vec` suitable for transmission or storage. + /// + /// Layout: `CT_LEN` || `NONCE_LEN` || `u32-LE aead_len` || `aead_ciphertext`. + pub fn to_bytes(&self) -> Vec { + let aead_len = self.aead_ciphertext.len() as u32; + let mut out = Vec::with_capacity(CT_LEN + NONCE_LEN + 4 + self.aead_ciphertext.len()); + out.extend_from_slice(&self.kem_ciphertext); + out.extend_from_slice(&self.nonce); + out.extend_from_slice(&aead_len.to_le_bytes()); + out.extend_from_slice(&self.aead_ciphertext); + out + } + + /// Deserialise from the flat byte format produced by [`EncryptedMessage::to_bytes`]. + /// + /// Returns `None` if the slice is too short or the embedded length field is + /// inconsistent with the remaining bytes. + pub fn from_bytes(bytes: &[u8]) -> Option { + // Minimum: CT_LEN + NONCE_LEN + 4-byte length field + let header_len = CT_LEN + NONCE_LEN + 4; + if bytes.len() < header_len { + return None; + } + let kem_ciphertext: [u8; CT_LEN] = bytes[..CT_LEN].try_into().ok()?; + let nonce: [u8; NONCE_LEN] = bytes[CT_LEN..CT_LEN + NONCE_LEN].try_into().ok()?; + let aead_len = u32::from_le_bytes( + bytes[CT_LEN + NONCE_LEN..CT_LEN + NONCE_LEN + 4].try_into().ok()?, + ) as usize; + let aead_start = CT_LEN + NONCE_LEN + 4; + if bytes.len() != aead_start + aead_len { + return None; + } + let aead_ciphertext = bytes[aead_start..].to_vec(); + Some(Self { kem_ciphertext, nonce, aead_ciphertext }) + } +} + +// ── Hybrid encrypt / decrypt ────────────────────────────────────────────────── + +/// Encrypt `plaintext` to a recipient identified by their ML-KEM-1024 +/// encapsulation key (`recipient_ek_bytes`, 1 568 bytes). +/// +/// Internally: +/// 1. Runs ML-KEM-1024 encapsulation to derive a 32-byte shared secret and a +/// 1 568-byte KEM ciphertext. +/// 2. Generates a random 12-byte nonce via `OsRng`. +/// 3. Seals `plaintext` with ChaCha20-Poly1305, using the shared secret as the +/// key and the random nonce. +/// +/// Returns an [`EncryptedMessage`] bundle containing everything the recipient +/// needs to decrypt. Never panics; returns [`PqEncryptError`] on any failure. +pub fn encrypt_to( + recipient_ek_bytes: &[u8], + plaintext: &[u8], +) -> Result { + // 1. KEM encapsulation — derive shared secret + KEM ciphertext. + let (ssk_bytes, kem_ciphertext) = encapsulate(recipient_ek_bytes)?; + + // 2. Build the AEAD key from the 32-byte ML-KEM shared secret. + let aead_key = Key::from(ssk_bytes); + let cipher = ChaCha20Poly1305::new(&aead_key); + + // 3. Generate a fresh random 12-byte nonce. + let nonce_bytes = generate_nonce()?; + let nonce = Nonce::from(nonce_bytes); + + // 4. Seal the plaintext. + let aead_ciphertext = cipher + .encrypt(&nonce, plaintext) + .map_err(|_| PqEncryptError::AeadError)?; + + Ok(EncryptedMessage { kem_ciphertext, nonce: nonce_bytes, aead_ciphertext }) +} + +/// Decrypt an [`EncryptedMessage`] using the holder's ML-KEM-1024 decapsulation +/// key (`dk_bytes`, 3 168 bytes). +/// +/// Internally: +/// 1. Runs ML-KEM-1024 decapsulation to recover the 32-byte shared secret. +/// 2. Opens the ChaCha20-Poly1305 AEAD ciphertext. +/// +/// Returns the plaintext on success, or `None` if: +/// * `dk_bytes` is malformed or the wrong length. +/// * The KEM ciphertext is structurally invalid (FIPS 203 implicit rejection +/// will have been applied — the AEAD open step then fails authentication). +/// * The AEAD authentication tag does not verify (tampered ciphertext, wrong key, +/// or wrong nonce). +/// +/// Deliberately returns `Option` rather than a typed error so that callers cannot +/// distinguish *why* decryption failed (KEM vs. AEAD), which reduces oracle +/// information available to an attacker. +pub fn decrypt(dk_bytes: &[u8], msg: &EncryptedMessage) -> Option> { + // 1. Recover the shared secret via ML-KEM decapsulation. + let ssk_bytes = decapsulate(dk_bytes, &msg.kem_ciphertext).ok()?; + + // 2. Reconstruct the AEAD cipher. + let aead_key = Key::from(ssk_bytes); + let cipher = ChaCha20Poly1305::new(&aead_key); + + // 3. Open the AEAD ciphertext. + let nonce = Nonce::from(msg.nonce); + cipher.decrypt(&nonce, msg.aead_ciphertext.as_ref()).ok() +} + +// ── Internal helper ─────────────────────────────────────────────────────────── + +/// Generate a fresh, cryptographically random 12-byte ChaCha20-Poly1305 nonce +/// using `AeadCore::generate_nonce` backed by the OS RNG (`OsRng`). +/// +/// This delegates entirely to the `aead` crate's OsRng integration, so no +/// separate `getrandom` dependency is required. +fn generate_nonce() -> Result<[u8; NONCE_LEN], PqEncryptError> { + // AeadCore::generate_nonce returns GenericArray; convert to [u8; 12]. + let nonce_ga = ChaCha20Poly1305::generate_nonce(&mut OsRng); + let nonce: [u8; NONCE_LEN] = nonce_ga.into(); + Ok(nonce) +} + +// ── File-based CLI helpers ────────────────────────────────────────────────────── + +/// Run `f` on a worker thread with a generous stack; ML-KEM matrix work can use more +/// stack than the main thread provides on some platforms. +fn on_big_stack(f: F) -> Result +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(f) + .map_err(|e| format!("failed to spawn crypto thread: {e}"))? + .join() + .map_err(|_| "crypto thread panicked".to_string()) +} + +/// `pq-kem-keygen`: write an ML-KEM-1024 keypair to `.ek` and `.dk`. +pub fn cli_keygen(out_prefix: &str) -> Result<(), String> { + let (ek, dk) = on_big_stack(keygen)?.map_err(|e| format!("ML-KEM keygen failed: {e:?}"))?; + let ek_path = format!("{out_prefix}.ek"); + let dk_path = format!("{out_prefix}.dk"); + std::fs::write(&ek_path, ek).map_err(|e| format!("write {ek_path}: {e}"))?; + std::fs::write(&dk_path, dk).map_err(|e| format!("write {dk_path}: {e}"))?; + println!("Generated ML-KEM-1024 (Kyber, FIPS 203) keypair:"); + println!(" encapsulation (public) key: {ek_path} ({EK_LEN} bytes)"); + println!(" decapsulation (secret) key: {dk_path} ({DK_LEN} bytes) [keep this file secret]"); + println!("\nEncrypt a file to this recipient with:"); + println!(" ghost pq-encrypt --to {ek_path} --in --out <ciphertext>"); + Ok(()) +} + +/// `pq-encrypt`: encrypt `in_path` to recipient `ek_path`, writing the bundle to `out_path`. +pub fn cli_encrypt(ek_path: &str, in_path: &str, out_path: &str) -> Result<(), String> { + let ek = std::fs::read(ek_path).map_err(|e| format!("read {ek_path}: {e}"))?; + let plaintext = std::fs::read(in_path).map_err(|e| format!("read {in_path}: {e}"))?; + let msg = on_big_stack(move || encrypt_to(&ek, &plaintext))? + .map_err(|e| format!("encryption failed: {e:?}"))?; + let bytes = msg.to_bytes(); + let n = bytes.len(); + std::fs::write(out_path, bytes).map_err(|e| format!("write {out_path}: {e}"))?; + println!("Encrypted {in_path} -> {out_path} ({n} bytes) for recipient {ek_path}"); + println!(" (ML-KEM-1024 encapsulation + ChaCha20-Poly1305 AEAD)"); + Ok(()) +} + +/// `pq-decrypt`: decrypt `in_path` with decapsulation key `dk_path` to `out_path`. +pub fn cli_decrypt(dk_path: &str, in_path: &str, out_path: &str) -> Result<(), String> { + let dk = std::fs::read(dk_path).map_err(|e| format!("read {dk_path}: {e}"))?; + let ct = std::fs::read(in_path).map_err(|e| format!("read {in_path}: {e}"))?; + let plaintext = on_big_stack(move || { + EncryptedMessage::from_bytes(&ct).and_then(|m| decrypt(&dk, &m)) + })? + .ok_or_else(|| { + "decryption failed (wrong key, tampered ciphertext, or malformed input)".to_string() + })?; + let n = plaintext.len(); + std::fs::write(out_path, plaintext).map_err(|e| format!("write {out_path}: {e}"))?; + println!("Decrypted {in_path} -> {out_path} ({n} bytes)"); + Ok(()) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + // ── Constant sanity — FIPS 203 spec values ──────────────────────────────── + + /// Verify that the byte-length constants we export match the FIPS 203 + /// specification for ML-KEM-1024 (Table 2, NIST SP 800-235 draft). + #[test] + fn fips203_ml_kem_1024_byte_lengths_match_spec() { + // Encapsulation key : 32(ρ) + 1536(t̂) = 1568 + assert_eq!(EK_LEN, 1568); + // Decapsulation key : 768(s) + 1568(ek) + 32(H(ek)) + 32(z) = 2400... wait + // FIPS 203 §7.1 Table 1: for k=4 (1024), DK byte length is 3168. + assert_eq!(DK_LEN, 3168); + // Ciphertext : 1408 + 160 = 1568 for k=4 + assert_eq!(CT_LEN, 1568); + // Shared secret : 32 for all ML-KEM variants + assert_eq!(SSK_LEN, 32); + // ChaCha20-Poly1305 nonce: 96 bits + assert_eq!(NONCE_LEN, 12); + } + + // ── ML-KEM-1024 encap/decap produce the same shared secret ─────────────── + + /// Core KEM property: the shared secret from `encapsulate` and from + /// `decapsulate` must be bit-for-bit identical. + #[test] + fn encap_decap_shared_secret_matches() { + let (ek_bytes, dk_bytes) = keygen().expect("keygen should succeed"); + + assert_eq!(ek_bytes.len(), EK_LEN); + assert_eq!(dk_bytes.len(), DK_LEN); + + let (ssk_enc, ct_bytes) = encapsulate(&ek_bytes).expect("encapsulate should succeed"); + assert_eq!(ct_bytes.len(), CT_LEN); + assert_eq!(ssk_enc.len(), SSK_LEN); + + let ssk_dec = decapsulate(&dk_bytes, &ct_bytes).expect("decapsulate should succeed"); + assert_eq!(ssk_dec.len(), SSK_LEN); + + // This is the fundamental KEM correctness property. + assert_eq!( + ssk_enc, ssk_dec, + "encapsulator and decapsulator must derive the same shared secret" + ); + } + + /// A second independent keygen produces a different keypair, and the two + /// shared secrets are different (probabilistic — collision chance 1/2^256). + #[test] + fn two_keygens_produce_distinct_keypairs() { + let (ek1, _) = keygen().unwrap(); + let (ek2, _) = keygen().unwrap(); + assert_ne!(ek1, ek2, "two independent keygens should yield different EKs"); + } + + // ── Hybrid encrypt/decrypt round-trip ───────────────────────────────────── + + /// End-to-end happy path: encrypt a payload then decrypt it and get back the + /// original plaintext. + #[test] + fn encrypt_decrypt_roundtrip() { + let (ek_bytes, dk_bytes) = keygen().unwrap(); + let plaintext = b"Ghost blockchain operator message - confidential."; + + let msg = encrypt_to(&ek_bytes, plaintext).expect("encrypt_to should succeed"); + + // The KEM ciphertext must be exactly CT_LEN bytes. + assert_eq!(msg.kem_ciphertext.len(), CT_LEN); + // The nonce must be exactly NONCE_LEN bytes. + assert_eq!(msg.nonce.len(), NONCE_LEN); + // AEAD ciphertext is plaintext + 16-byte Poly1305 tag. + assert_eq!(msg.aead_ciphertext.len(), plaintext.len() + 16); + + let recovered = decrypt(&dk_bytes, &msg).expect("decrypt should succeed"); + assert_eq!(recovered, plaintext); + } + + /// Empty plaintext is a valid input. + #[test] + fn encrypt_decrypt_empty_plaintext() { + let (ek_bytes, dk_bytes) = keygen().unwrap(); + let msg = encrypt_to(&ek_bytes, b"").unwrap(); + let recovered = decrypt(&dk_bytes, &msg).unwrap(); + assert!(recovered.is_empty()); + } + + /// Large plaintext (64 KiB) is handled correctly — exercises the streaming + /// path through ChaCha20-Poly1305. + #[test] + fn encrypt_decrypt_large_payload() { + let (ek_bytes, dk_bytes) = keygen().unwrap(); + let plaintext = vec![0xABu8; 65536]; + let msg = encrypt_to(&ek_bytes, &plaintext).unwrap(); + let recovered = decrypt(&dk_bytes, &msg).unwrap(); + assert_eq!(recovered, plaintext); + } + + // ── Wrong decapsulation key fails ───────────────────────────────────────── + + /// Decrypting with a different (wrong) decapsulation key must fail. + /// + /// FIPS 203 §8.3 specifies "implicit rejection": decapsulating with the + /// wrong key returns a pseudo-random shared secret. The AEAD open step + /// will then fail authentication because the derived AEAD key is wrong. + #[test] + fn wrong_dk_returns_none() { + let (ek1, _dk1) = keygen().unwrap(); + let (_ek2, dk2) = keygen().unwrap(); + + let plaintext = b"secret payload"; + let msg = encrypt_to(&ek1, plaintext).unwrap(); + + // dk2 belongs to a different keypair — AEAD authentication must fail. + let result = decrypt(&dk2, &msg); + assert!( + result.is_none(), + "decrypting with the wrong DK must return None" + ); + } + + // ── Tampered AEAD ciphertext fails ──────────────────────────────────────── + + /// Flipping one bit in the AEAD ciphertext must cause authentication to fail. + #[test] + fn tampered_aead_ciphertext_returns_none() { + let (ek_bytes, dk_bytes) = keygen().unwrap(); + let plaintext = b"tamper-detection test"; + let mut msg = encrypt_to(&ek_bytes, plaintext).unwrap(); + + // Flip the last byte of the AEAD ciphertext (part of the Poly1305 tag). + let last = msg.aead_ciphertext.last_mut().expect("non-empty AEAD ciphertext"); + *last ^= 0xFF; + + let result = decrypt(&dk_bytes, &msg); + assert!( + result.is_none(), + "tampered AEAD ciphertext must return None" + ); + } + + /// Flipping one bit in the *body* of the AEAD ciphertext (not the tag) + /// must also fail — Poly1305 covers the full ciphertext. + #[test] + fn tampered_aead_body_returns_none() { + let (ek_bytes, dk_bytes) = keygen().unwrap(); + let plaintext = b"tamper-detection body test - long enough to have body bytes"; + let mut msg = encrypt_to(&ek_bytes, plaintext).unwrap(); + + // Flip a byte in the middle (ciphertext body, before the 16-byte tag). + msg.aead_ciphertext[0] ^= 0x01; + + assert!(decrypt(&dk_bytes, &msg).is_none()); + } + + /// Replacing the nonce with a different nonce must cause decryption to fail. + #[test] + fn wrong_nonce_returns_none() { + let (ek_bytes, dk_bytes) = keygen().unwrap(); + let plaintext = b"nonce-mismatch test"; + let mut msg = encrypt_to(&ek_bytes, plaintext).unwrap(); + + // Flip every bit of the nonce. + for byte in &mut msg.nonce { + *byte ^= 0xFF; + } + + assert!( + decrypt(&dk_bytes, &msg).is_none(), + "wrong nonce must prevent decryption" + ); + } + + // ── Serialisation round-trip of EncryptedMessage ───────────────────────── + + /// `to_bytes` / `from_bytes` must be inverses of each other. + #[test] + fn encrypted_message_wire_roundtrip() { + let (ek_bytes, dk_bytes) = keygen().unwrap(); + let plaintext = b"wire-format round-trip"; + let msg = encrypt_to(&ek_bytes, plaintext).unwrap(); + + let wire = msg.to_bytes(); + let decoded = EncryptedMessage::from_bytes(&wire) + .expect("from_bytes should succeed on valid wire data"); + + assert_eq!(msg, decoded); + + // And decrypting after the round-trip still works. + let recovered = decrypt(&dk_bytes, &decoded).unwrap(); + assert_eq!(recovered.as_slice(), plaintext); + } + + /// Truncated wire data must not panic and must return None. + #[test] + fn from_bytes_truncated_returns_none() { + let wire = vec![0u8; CT_LEN + NONCE_LEN]; // missing length field + assert!(EncryptedMessage::from_bytes(&wire).is_none()); + } + + // ── API error paths — bad input lengths ─────────────────────────────────── + + #[test] + fn encapsulate_rejects_wrong_ek_length() { + let result = encapsulate(&[0u8; 16]); + assert_eq!(result, Err(PqEncryptError::BadLength(EK_LEN, 16))); + } + + #[test] + fn decapsulate_rejects_wrong_dk_length() { + let result = decapsulate(&[0u8; 16], &[0u8; CT_LEN]); + assert_eq!(result, Err(PqEncryptError::BadLength(DK_LEN, 16))); + } + + #[test] + fn decapsulate_rejects_wrong_ct_length() { + let (_ek, dk) = keygen().unwrap(); + let result = decapsulate(&dk, &[0u8; 16]); + assert_eq!(result, Err(PqEncryptError::BadLength(CT_LEN, 16))); + } + + #[test] + fn encrypt_to_rejects_wrong_ek_length() { + let result = encrypt_to(&[0u8; 100], b"msg"); + assert_eq!(result, Err(PqEncryptError::BadLength(EK_LEN, 100))); + } +} diff --git a/node/src/rpc.rs b/node/src/rpc.rs index 1fc6eb0..fe2b6ca 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -14,12 +14,16 @@ use sp_api::ProvideRuntimeApi; use sp_block_builder::BlockBuilder; use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; +pub use sc_rpc_api::DenyUnsafe; + /// Full client dependencies. pub struct FullDeps<C, P> { /// The client instance to use. pub client: Arc<C>, /// Transaction pool instance. pub pool: Arc<P>, + /// Whether to deny unsafe calls + pub deny_unsafe: DenyUnsafe, } /// Instantiate all full RPC extensions. @@ -39,9 +43,9 @@ where use substrate_frame_rpc_system::{System, SystemApiServer}; let mut module = RpcModule::new(()); - let FullDeps { client, pool } = deps; + let FullDeps { client, pool, deny_unsafe } = deps; - module.merge(System::new(client.clone(), pool).into_rpc())?; + module.merge(System::new(client.clone(), pool, deny_unsafe).into_rpc())?; module.merge(TransactionPayment::new(client).into_rpc())?; // Extend this RPC with a custom API by using the following syntax. diff --git a/node/src/service.rs b/node/src/service.rs index 6e97e4e..5759124 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -1,334 +1,294 @@ -//! Service and ServiceFactory implementation. Specialized wrapper over substrate service. +//! Service and ServiceFactory implementation — Ghost Proof-of-Work edition. +//! +//! Block authoring is real Proof-of-Work via `sc-consensus-pow`. There is no Aura slot +//! scheduler and no GRANDPA finality gadget: the canonical chain is the one with the most +//! accumulated PoW (longest-/heaviest-chain). Difficulty is read from the runtime +//! (`sp_consensus_pow::DifficultyApi`), where `pallet-ghost-consensus` retargets it toward +//! the target block time. The sc-service scaffolding here mirrors the stable2407 solochain +//! template; only the consensus engine differs. +use std::{sync::Arc, time::Duration}; + +use codec::Encode; use futures::FutureExt; -use sc_client_api::{Backend, BlockBackend}; -use sc_consensus_aura::{ImportQueueParams, SlotProportion, StartAuraParams}; -use sc_consensus_grandpa::SharedVoterState; -use sc_service::{error::Error as ServiceError, Configuration, TaskManager, WarpSyncConfig}; +use sc_client_api::Backend; +use sc_consensus::BoxBlockImport; +use sc_service::{error::Error as ServiceError, Configuration, TaskManager}; use sc_telemetry::{Telemetry, TelemetryWorker}; use sc_transaction_pool_api::OffchainTransactionPoolFactory; use solochain_template_runtime::{self, apis::RuntimeApi, opaque::Block}; -use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; -use std::{sync::Arc, time::Duration}; +use crate::pow::{meets_difficulty, pow_hash, GhostPow, GhostSeal}; pub(crate) type FullClient = sc_service::TFullClient< - Block, - RuntimeApi, - sc_executor::WasmExecutor<sp_io::SubstrateHostFunctions>, + Block, + RuntimeApi, + sc_executor::WasmExecutor<sp_io::SubstrateHostFunctions>, >; type FullBackend = sc_service::TFullBackend<Block>; type FullSelectChain = sc_consensus::LongestChain<FullBackend, Block>; -/// The minimum period of blocks on which justifications will be -/// imported and generated. -const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512; - pub type Service = sc_service::PartialComponents< - FullClient, - FullBackend, - FullSelectChain, - sc_consensus::DefaultImportQueue<Block>, - sc_transaction_pool::TransactionPoolHandle<Block, FullClient>, - ( - sc_consensus_grandpa::GrandpaBlockImport<FullBackend, Block, FullClient, FullSelectChain>, - sc_consensus_grandpa::LinkHalf<Block, FullClient, FullSelectChain>, - Option<Telemetry>, - ), + FullClient, + FullBackend, + FullSelectChain, + sc_consensus::DefaultImportQueue<Block>, + sc_transaction_pool::FullPool<Block, FullClient>, + ( + BoxBlockImport<Block>, + GhostPow<FullClient>, + Option<Telemetry>, + ), >; pub fn new_partial(config: &Configuration) -> Result<Service, ServiceError> { - let telemetry = config - .telemetry_endpoints - .clone() - .filter(|x| !x.is_empty()) - .map(|endpoints| -> Result<_, sc_telemetry::Error> { - let worker = TelemetryWorker::new(16)?; - let telemetry = worker.handle().new_telemetry(endpoints); - Ok((worker, telemetry)) - }) - .transpose()?; - - let executor = sc_service::new_wasm_executor::<sp_io::SubstrateHostFunctions>(&config.executor); - let (client, backend, keystore_container, task_manager) = - sc_service::new_full_parts::<Block, RuntimeApi, _>( - config, - telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), - executor, - )?; - let client = Arc::new(client); - - let telemetry = telemetry.map(|(worker, telemetry)| { - task_manager.spawn_handle().spawn("telemetry", None, worker.run()); - telemetry - }); - - let select_chain = sc_consensus::LongestChain::new(backend.clone()); - - let transaction_pool = Arc::from( - sc_transaction_pool::Builder::new( - task_manager.spawn_essential_handle(), - client.clone(), - config.role.is_authority().into(), - ) - .with_options(config.transaction_pool.clone()) - .with_prometheus(config.prometheus_registry()) - .build(), - ); - - let (grandpa_block_import, grandpa_link) = sc_consensus_grandpa::block_import( - client.clone(), - GRANDPA_JUSTIFICATION_PERIOD, - &client, - select_chain.clone(), - telemetry.as_ref().map(|x| x.handle()), - )?; - - let cidp_client = client.clone(); - let import_queue = - sc_consensus_aura::import_queue::<AuraPair, _, _, _, _, _>(ImportQueueParams { - block_import: grandpa_block_import.clone(), - justification_import: Some(Box::new(grandpa_block_import.clone())), - client: client.clone(), - create_inherent_data_providers: move |parent_hash, _| { - let cidp_client = cidp_client.clone(); - async move { - let slot_duration = sc_consensus_aura::standalone::slot_duration_at( - &*cidp_client, - parent_hash, - )?; - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - - let slot = - sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration( - *timestamp, - slot_duration, - ); - - Ok((slot, timestamp)) - } - }, - spawner: &task_manager.spawn_essential_handle(), - registry: config.prometheus_registry(), - check_for_equivocation: Default::default(), - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - })?; - - Ok(sc_service::PartialComponents { - client, - backend, - task_manager, - import_queue, - keystore_container, - select_chain, - transaction_pool, - other: (grandpa_block_import, grandpa_link, telemetry), - }) + let telemetry = config + .telemetry_endpoints + .clone() + .filter(|x| !x.is_empty()) + .map(|endpoints| -> Result<_, sc_telemetry::Error> { + let worker = TelemetryWorker::new(16)?; + let telemetry = worker.handle().new_telemetry(endpoints); + Ok((worker, telemetry)) + }) + .transpose()?; + + let executor = sc_service::new_wasm_executor::<sp_io::SubstrateHostFunctions>(config); + let (client, backend, keystore_container, task_manager) = + sc_service::new_full_parts::<Block, RuntimeApi, _>( + config, + telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), + executor, + )?; + let client = Arc::new(client); + + let telemetry = telemetry.map(|(worker, telemetry)| { + task_manager + .spawn_handle() + .spawn("telemetry", None, worker.run()); + telemetry + }); + + let select_chain = sc_consensus::LongestChain::new(backend.clone()); + + let transaction_pool = sc_transaction_pool::BasicPool::new_full( + config.transaction_pool.clone(), + config.role.is_authority().into(), + config.prometheus_registry(), + task_manager.spawn_essential_handle(), + client.clone(), + ); + + let algorithm = GhostPow::new(client.clone()); + + let pow_block_import = sc_consensus_pow::PowBlockImport::new( + client.clone(), + client.clone(), + algorithm.clone(), + 0u32, + select_chain.clone(), + |_parent, ()| async { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + Ok::<_, Box<dyn std::error::Error + Send + Sync>>(timestamp) + }, + ); + + let import_queue = sc_consensus_pow::import_queue( + Box::new(pow_block_import.clone()), + None, + algorithm.clone(), + &task_manager.spawn_essential_handle(), + config.prometheus_registry(), + )?; + + Ok(sc_service::PartialComponents { + client, + backend, + task_manager, + import_queue, + keystore_container, + select_chain, + transaction_pool, + other: (Box::new(pow_block_import), algorithm, telemetry), + }) } /// Builds a new service for a full client. pub fn new_full< - N: sc_network::NetworkBackend<Block, <Block as sp_runtime::traits::Block>::Hash>, + N: sc_network::NetworkBackend<Block, <Block as sp_runtime::traits::Block>::Hash>, >( - config: Configuration, + config: Configuration, ) -> Result<TaskManager, ServiceError> { - let sc_service::PartialComponents { - client, - backend, - mut task_manager, - import_queue, - keystore_container, - select_chain, - transaction_pool, - other: (block_import, grandpa_link, mut telemetry), - } = new_partial(&config)?; - - let mut net_config = sc_network::config::FullNetworkConfiguration::< - Block, - <Block as sp_runtime::traits::Block>::Hash, - N, - >::new(&config.network, config.prometheus_registry().cloned()); - let metrics = N::register_notification_metrics(config.prometheus_registry()); - - let peer_store_handle = net_config.peer_store_handle(); - let grandpa_protocol_name = sc_consensus_grandpa::protocol_standard_name( - &client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"), - &config.chain_spec, - ); - let (grandpa_protocol_config, grandpa_notification_service) = - sc_consensus_grandpa::grandpa_peers_set_config::<_, N>( - grandpa_protocol_name.clone(), - metrics.clone(), - peer_store_handle, - ); - net_config.add_notification_protocol(grandpa_protocol_config); - - let warp_sync = Arc::new(sc_consensus_grandpa::warp_proof::NetworkProvider::new( - backend.clone(), - grandpa_link.shared_authority_set().clone(), - Vec::default(), - )); - - let (network, system_rpc_tx, tx_handler_controller, sync_service) = - sc_service::build_network(sc_service::BuildNetworkParams { - config: &config, - net_config, - client: client.clone(), - transaction_pool: transaction_pool.clone(), - spawn_handle: task_manager.spawn_handle(), - import_queue, - block_announce_validator_builder: None, - warp_sync_config: Some(WarpSyncConfig::WithProvider(warp_sync)), - block_relay: None, - metrics, - })?; - - if config.offchain_worker.enabled { - let offchain_workers = - sc_offchain::OffchainWorkers::new(sc_offchain::OffchainWorkerOptions { - runtime_api_provider: client.clone(), - is_validator: config.role.is_authority(), - keystore: Some(keystore_container.keystore()), - offchain_db: backend.offchain_storage(), - transaction_pool: Some(OffchainTransactionPoolFactory::new( - transaction_pool.clone(), - )), - network_provider: Arc::new(network.clone()), - enable_http_requests: true, - custom_extensions: |_| vec![], - })?; - task_manager.spawn_handle().spawn( - "offchain-workers-runner", - "offchain-worker", - offchain_workers.run(client.clone(), task_manager.spawn_handle()).boxed(), - ); - } - - let role = config.role; - let force_authoring = config.force_authoring; - let backoff_authoring_blocks: Option<()> = None; - let name = config.network.node_name.clone(); - let enable_grandpa = !config.disable_grandpa; - let prometheus_registry = config.prometheus_registry().cloned(); - - let rpc_extensions_builder = { - let client = client.clone(); - let pool = transaction_pool.clone(); - - Box::new(move |_| { - let deps = crate::rpc::FullDeps { client: client.clone(), pool: pool.clone() }; - crate::rpc::create_full(deps).map_err(Into::into) - }) - }; - - let _rpc_handlers = sc_service::spawn_tasks(sc_service::SpawnTasksParams { - network: Arc::new(network.clone()), - client: client.clone(), - keystore: keystore_container.keystore(), - task_manager: &mut task_manager, - transaction_pool: transaction_pool.clone(), - rpc_builder: rpc_extensions_builder, - backend, - system_rpc_tx, - tx_handler_controller, - sync_service: sync_service.clone(), - config, - telemetry: telemetry.as_mut(), - })?; - - if role.is_authority() { - let proposer_factory = sc_basic_authorship::ProposerFactory::new( - task_manager.spawn_handle(), - client.clone(), - transaction_pool.clone(), - prometheus_registry.as_ref(), - telemetry.as_ref().map(|x| x.handle()), - ); - - let slot_duration = sc_consensus_aura::slot_duration(&*client)?; - - let aura = sc_consensus_aura::start_aura::<AuraPair, _, _, _, _, _, _, _, _, _, _>( - StartAuraParams { - slot_duration, - client, - select_chain, - block_import, - proposer_factory, - create_inherent_data_providers: move |_, ()| async move { - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - - let slot = - sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration( - *timestamp, - slot_duration, - ); - - Ok((slot, timestamp)) - }, - force_authoring, - backoff_authoring_blocks, - keystore: keystore_container.keystore(), - sync_oracle: sync_service.clone(), - justification_sync_link: sync_service.clone(), - block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), - max_block_proposal_slot_portion: None, - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - }, - )?; - - // the AURA authoring task is considered essential, i.e. if it - // fails we take down the service with it. - task_manager - .spawn_essential_handle() - .spawn_blocking("aura", Some("block-authoring"), aura); - } - - if enable_grandpa { - // if the node isn't actively participating in consensus then it doesn't - // need a keystore, regardless of which protocol we use below. - let keystore = if role.is_authority() { Some(keystore_container.keystore()) } else { None }; - - let grandpa_config = sc_consensus_grandpa::Config { - // FIXME #1578 make this available through chainspec - gossip_duration: Duration::from_millis(333), - justification_generation_period: GRANDPA_JUSTIFICATION_PERIOD, - name: Some(name), - observer_enabled: false, - keystore, - local_role: role, - telemetry: telemetry.as_ref().map(|x| x.handle()), - protocol_name: grandpa_protocol_name, - }; - - // start the full GRANDPA voter - // NOTE: non-authorities could run the GRANDPA observer protocol, but at - // this point the full voter should provide better guarantees of block - // and vote data availability than the observer. The observer has not - // been tested extensively yet and having most nodes in a network run it - // could lead to finality stalls. - let grandpa_config = sc_consensus_grandpa::GrandpaParams { - config: grandpa_config, - link: grandpa_link, - network, - sync: Arc::new(sync_service), - notification_service: grandpa_notification_service, - voting_rule: sc_consensus_grandpa::VotingRulesBuilder::default().build(), - prometheus_registry, - shared_voter_state: SharedVoterState::empty(), - telemetry: telemetry.as_ref().map(|x| x.handle()), - offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(transaction_pool), - }; - - // the GRANDPA voter task is considered infallible, i.e. - // if it fails we take down the service with it. - task_manager.spawn_essential_handle().spawn_blocking( - "grandpa-voter", - None, - sc_consensus_grandpa::run_grandpa_voter(grandpa_config)?, - ); - } - - Ok(task_manager) + let sc_service::PartialComponents { + client, + backend, + mut task_manager, + import_queue, + keystore_container, + select_chain, + transaction_pool, + other: (pow_block_import, algorithm, mut telemetry), + } = new_partial(&config)?; + + let mut net_config = sc_network::config::FullNetworkConfiguration::< + Block, + <Block as sp_runtime::traits::Block>::Hash, + N, + >::new(&config.network); + let metrics = N::register_notification_metrics(config.prometheus_registry()); + + let (network, system_rpc_tx, tx_handler_controller, network_starter, sync_service) = + sc_service::build_network(sc_service::BuildNetworkParams { + config: &config, + net_config, + client: client.clone(), + transaction_pool: transaction_pool.clone(), + spawn_handle: task_manager.spawn_handle(), + import_queue, + block_announce_validator_builder: None, + warp_sync_params: None, + block_relay: None, + metrics, + })?; + + if config.offchain_worker.enabled { + task_manager.spawn_handle().spawn( + "offchain-workers-runner", + "offchain-worker", + sc_offchain::OffchainWorkers::new(sc_offchain::OffchainWorkerOptions { + runtime_api_provider: client.clone(), + is_validator: config.role.is_authority(), + keystore: Some(keystore_container.keystore()), + offchain_db: backend.offchain_storage(), + transaction_pool: Some(OffchainTransactionPoolFactory::new( + transaction_pool.clone(), + )), + network_provider: Arc::new(network.clone()), + enable_http_requests: true, + custom_extensions: |_| vec![], + }) + .run(client.clone(), task_manager.spawn_handle()) + .boxed(), + ); + } + + let role = config.role.clone(); + let prometheus_registry = config.prometheus_registry().cloned(); + + let rpc_extensions_builder = { + let client = client.clone(); + let pool = transaction_pool.clone(); + + Box::new(move |deny_unsafe, _| { + let deps = crate::rpc::FullDeps { + client: client.clone(), + pool: pool.clone(), + deny_unsafe, + }; + crate::rpc::create_full(deps).map_err(Into::into) + }) + }; + + let _rpc_handlers = sc_service::spawn_tasks(sc_service::SpawnTasksParams { + network: Arc::new(network.clone()), + client: client.clone(), + keystore: keystore_container.keystore(), + task_manager: &mut task_manager, + transaction_pool: transaction_pool.clone(), + rpc_builder: rpc_extensions_builder, + backend, + system_rpc_tx, + tx_handler_controller, + sync_service: sync_service.clone(), + config, + telemetry: telemetry.as_mut(), + })?; + + if role.is_authority() { + let proposer_factory = sc_basic_authorship::ProposerFactory::new( + task_manager.spawn_handle(), + client.clone(), + transaction_pool.clone(), + prometheus_registry.as_ref(), + telemetry.as_ref().map(|x| x.handle()), + ); + + let (mining_handle, mining_task) = sc_consensus_pow::start_mining_worker( + pow_block_import, + client.clone(), + select_chain, + algorithm, + proposer_factory, + sync_service.clone(), + sync_service.clone(), + None, + |_parent, ()| async { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + Ok::<_, Box<dyn std::error::Error + Send + Sync>>(timestamp) + }, + Duration::from_secs(10), + Duration::from_secs(10), + ); + + task_manager.spawn_essential_handle().spawn_blocking( + "pow-mining-worker", + Some("pow"), + mining_task, + ); + + // CPU miner threads run OFF the async executor: the mining worker's `submit` + // holds a non-Send lock across an `.await`, so it cannot be a Send async task. + // Each thread drives `submit` to completion with `block_on`, and searches a + // disjoint slice of the nonce space (offset by thread id, stepping by the + // thread count) so threads never duplicate work. + let threads = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1); + for thread_id in 0..threads { + let mining_handle = mining_handle.clone(); + let step = threads as u64; + std::thread::Builder::new() + .name(format!("ghost-pow-miner-{thread_id}")) + .spawn(move || { + let mut current_pre: Option<sp_core::H256> = None; + let mut nonce = thread_id as u64; + loop { + let metadata = match mining_handle.metadata() { + Some(metadata) => metadata, + None => { + std::thread::sleep(Duration::from_millis(300)); + continue; + } + }; + + if current_pre != Some(metadata.pre_hash) { + current_pre = Some(metadata.pre_hash); + nonce = thread_id as u64; + } + + let mut solved = None; + for _ in 0..50_000u64 { + if meets_difficulty( + &pow_hash(&metadata.pre_hash, nonce), + metadata.difficulty, + ) { + solved = Some(nonce); + break; + } + nonce = nonce.wrapping_add(step); + } + + if let Some(found) = solved { + let seal = GhostSeal { nonce: found }.encode(); + let _ = futures::executor::block_on(mining_handle.submit(seal)); + std::thread::sleep(Duration::from_millis(50)); + } + } + }) + .expect("ghost PoW miner thread spawns"); + } + } + + network_starter.start_network(); + Ok(task_manager) } diff --git a/node/src/wallet.rs b/node/src/wallet.rs new file mode 100644 index 0000000..cd96374 --- /dev/null +++ b/node/src/wallet.rs @@ -0,0 +1,364 @@ +//! Minimal RPC wallet / transactor for the Ghost node CLI. +//! +//! Connects to a running node's JSON-RPC endpoint (default `http://127.0.0.1:9944`) to: +//! - query live balances and the on-chain validator/stake set (read-only), and +//! - build, sign (sr25519), and submit real extrinsics (`stake`, `unstake`, `transfer`) +//! using the runtime's own `RuntimeCall`/`UncheckedExtrinsic`/`TxExtension` types, so the +//! SCALE encoding always matches the runtime this binary was built from. +//! +//! Signing uses `SignedPayload::from_raw` with the additional-signed data supplied locally +//! (spec/tx version from the embedded `VERSION`, genesis hash fetched over RPC), so it needs +//! no local chain state. Era is immortal and the metadata-hash extension is disabled, which +//! matches a node run without the `metadata-hash` feature. + +use codec::{Decode, Encode}; +use fips204::{ml_dsa_87, traits::SerDes}; +use jsonrpsee::core::client::ClientT; +use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; +use jsonrpsee::rpc_params; +use sp_core::{ + blake2_128, + bytes::{from_hex, to_hex}, + crypto::{Pair, Ss58Codec}, + sr25519, twox_128, H256, +}; +use sp_runtime::{ + generic::Era, traits::IdentifyAccount, MultiAddress, MultiSignature, MultiSigner, +}; + +use solochain_template_runtime::{ + AccountId, Balance, Runtime, RuntimeCall, SignedPayload, TxExtension, UncheckedExtrinsic, + VERSION, +}; + +/// Default node RPC endpoint (Substrate serves HTTP + WS on the same port). +pub const DEFAULT_RPC: &str = "http://127.0.0.1:9944"; +/// Default signer when none is given (development account). +pub const DEFAULT_SURI: &str = "//Alice"; + +/// Local mirror of `frame_system::AccountInfo<u32, pallet_balances::AccountData<Balance>>` +/// so the `System.Account` storage value can be decoded without extra type plumbing. +#[derive(Decode)] +struct AccountData { + free: Balance, + reserved: Balance, + frozen: Balance, + _flags: u128, +} + +#[derive(Decode)] +struct AccountInfo { + nonce: u32, + _consumers: u32, + _providers: u32, + _sufficients: u32, + data: AccountData, +} + +fn run<F: core::future::Future>(f: F) -> F::Output { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to start a tokio runtime for the RPC client") + .block_on(f) +} + +fn connect(rpc: &str) -> Result<HttpClient, String> { + HttpClientBuilder::default() + .build(rpc) + .map_err(|e| format!("failed to connect to node RPC at {rpc}: {e}")) +} + +/// Derive (pair, account) from a secret URI / dev seed such as `//Alice` or a mnemonic. +fn signer(suri: &str) -> Result<(sr25519::Pair, AccountId), String> { + let pair = sr25519::Pair::from_string(suri, None) + .map_err(|e| format!("invalid signer secret '{suri}': {e:?}"))?; + let account = MultiSigner::Sr25519(pair.public()).into_account(); + Ok((pair, account)) +} + +/// Resolve an account from an SS58 address, or from a secret URI / dev seed. +fn resolve_account(s: &str) -> Result<AccountId, String> { + if let Ok(acc) = AccountId::from_ss58check(s) { + return Ok(acc); + } + let pair = sr25519::Pair::from_string(s, None) + .map_err(|e| format!("'{s}' is not a valid SS58 address or secret URI: {e:?}"))?; + Ok(MultiSigner::Sr25519(pair.public()).into_account()) +} + +fn map_prefix(pallet: &str, item: &str) -> Vec<u8> { + let mut k = Vec::with_capacity(32); + k.extend_from_slice(&twox_128(pallet.as_bytes())); + k.extend_from_slice(&twox_128(item.as_bytes())); + k +} + +/// Storage key for a `Blake2_128Concat` map entry (used by both `System.Account` and +/// `GhostConsensus.ValidatorStakes`). +fn blake2_map_key(pallet: &str, item: &str, raw_key: &[u8]) -> Vec<u8> { + let mut k = map_prefix(pallet, item); + k.extend_from_slice(&blake2_128(raw_key)); + k.extend_from_slice(raw_key); + k +} + +// --------------------------------------------------------------------------- +// Async RPC primitives +// --------------------------------------------------------------------------- + +async fn rpc_next_nonce(c: &HttpClient, account: &AccountId) -> Result<u32, String> { + c.request("system_accountNextIndex", rpc_params![account.to_ss58check()]) + .await + .map_err(|e| format!("system_accountNextIndex failed: {e}")) +} + +async fn rpc_genesis_hash(c: &HttpClient) -> Result<H256, String> { + let hex: String = c + .request("chain_getBlockHash", rpc_params![0u32]) + .await + .map_err(|e| format!("chain_getBlockHash(0) failed: {e}"))?; + Ok(H256::from_slice(&from_hex(&hex).map_err(|e| format!("bad genesis hash: {e:?}"))?)) +} + +async fn rpc_get_storage(c: &HttpClient, key: &[u8]) -> Result<Option<Vec<u8>>, String> { + let res: Option<String> = c + .request("state_getStorage", rpc_params![to_hex(key, false)]) + .await + .map_err(|e| format!("state_getStorage failed: {e}"))?; + match res { + Some(h) => Ok(Some(from_hex(&h).map_err(|e| format!("bad storage hex: {e:?}"))?)), + None => Ok(None), + } +} + +async fn rpc_keys_paged(c: &HttpClient, prefix: &[u8]) -> Result<Vec<Vec<u8>>, String> { + let prefix_hex = to_hex(prefix, false); + let mut out: Vec<Vec<u8>> = Vec::new(); + let mut start: Option<String> = None; + loop { + let page: Vec<String> = c + .request( + "state_getKeysPaged", + rpc_params![prefix_hex.clone(), 256u32, start.clone()], + ) + .await + .map_err(|e| format!("state_getKeysPaged failed: {e}"))?; + let count = page.len(); + start = page.last().cloned(); + for k in page { + out.push(from_hex(&k).map_err(|e| format!("bad key hex: {e:?}"))?); + } + if count < 256 { + break; + } + } + Ok(out) +} + +async fn rpc_submit(c: &HttpClient, xt: &UncheckedExtrinsic) -> Result<H256, String> { + let hex: String = c + .request("author_submitExtrinsic", rpc_params![to_hex(&xt.encode(), false)]) + .await + .map_err(|e| format!("the node rejected the transaction: {e}"))?; + Ok(H256::from_slice(&from_hex(&hex).map_err(|e| format!("bad tx hash: {e:?}"))?)) +} + +/// Build a signed extrinsic using the runtime's own extension types. `additional` mirrors +/// each extension's implicit data; `SignedPayload::from_raw` avoids needing chain state. +fn build_signed( + call: RuntimeCall, + pair: &sr25519::Pair, + account: &AccountId, + nonce: u32, + genesis: H256, +) -> Result<UncheckedExtrinsic, String> { + let extra: TxExtension = ( + frame_system::CheckNonZeroSender::<Runtime>::new(), + frame_system::CheckSpecVersion::<Runtime>::new(), + frame_system::CheckTxVersion::<Runtime>::new(), + frame_system::CheckGenesis::<Runtime>::new(), + frame_system::CheckEra::<Runtime>::from(Era::Immortal), + frame_system::CheckNonce::<Runtime>::from(nonce), + frame_system::CheckWeight::<Runtime>::new(), + pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(0), + frame_metadata_hash_extension::CheckMetadataHash::<Runtime>::new(false), + ); + + let additional = ( + (), + VERSION.spec_version, + VERSION.transaction_version, + genesis, + genesis, // immortal era anchors at the genesis hash + (), + (), + (), + None::<[u8; 32]>, // metadata-hash extension disabled + ); + + let payload = SignedPayload::from_raw(call.clone(), extra.clone(), additional); + let signature = payload.using_encoded(|p| pair.sign(p)); + + Ok(UncheckedExtrinsic::new_signed( + call, + MultiAddress::Id(account.clone()), + MultiSignature::Sr25519(signature), + extra, + )) +} + +// --------------------------------------------------------------------------- +// Public command entry points (sync wrappers used by the CLI handlers) +// --------------------------------------------------------------------------- + +/// Print live free/reserved/frozen balance and nonce for an account. +pub fn show_balance(account: &str, rpc: Option<&str>) -> Result<(), String> { + let rpc = rpc.unwrap_or(DEFAULT_RPC).to_string(); + let acc = resolve_account(account)?; + run(async move { + let c = connect(&rpc)?; + let key = blake2_map_key("System", "Account", &acc.encode()); + println!("Account: {}", acc.to_ss58check()); + match rpc_get_storage(&c, &key).await? { + Some(bytes) => { + let info = AccountInfo::decode(&mut &bytes[..]) + .map_err(|e| format!("failed to decode account info: {e}"))?; + println!(" Free: {}", info.data.free); + println!(" Reserved: {}", info.data.reserved); + println!(" Frozen: {}", info.data.frozen); + println!(" Nonce: {}", info.nonce); + } + None => println!(" (no on-chain data; account is empty / never used)"), + } + Ok(()) + }) +} + +/// List the live validator set (accounts with a stake in `GhostConsensus.ValidatorStakes`). +pub fn list_validators(rpc: Option<&str>) -> Result<(), String> { + let rpc = rpc.unwrap_or(DEFAULT_RPC).to_string(); + run(async move { + let c = connect(&rpc)?; + let prefix = map_prefix("GhostConsensus", "ValidatorStakes"); + let keys = rpc_keys_paged(&c, &prefix).await?; + if keys.is_empty() { + println!("No validators are currently staked."); + return Ok(()); + } + println!("Staked validators ({}):", keys.len()); + for key in keys { + if key.len() < 32 { + continue; + } + // Key layout: 16-byte twox(pallet) + 16-byte twox(item) + 16-byte blake2 + AccountId. + let acc = AccountId::decode(&mut &key[key.len() - 32..]) + .map_err(|e| format!("failed to decode validator account: {e}"))?; + let stake = match rpc_get_storage(&c, &key).await? { + Some(b) => Balance::decode(&mut &b[..]).unwrap_or(0), + None => 0, + }; + // Does this validator have a registered ML-DSA-87 attestation key? + let pq_key = blake2_map_key("GhostConsensus", "ValidatorMlDsaKey", &acc.encode()); + let pq = match rpc_get_storage(&c, &pq_key).await? { + Some(b) => match Vec::<u8>::decode(&mut &b[..]) { + Ok(v) => format!("ML-DSA-87 key registered ({} bytes)", v.len()), + Err(_) => "ML-DSA-87 key registered".to_string(), + }, + None => "no PQ key".to_string(), + }; + println!(" {} stake {} [{}]", acc.to_ss58check(), stake, pq); + } + Ok(()) + }) +} + +fn submit_call(call: RuntimeCall, suri: &str, rpc: Option<&str>, what: &str) -> Result<(), String> { + let rpc = rpc.unwrap_or(DEFAULT_RPC).to_string(); + let (pair, account) = signer(suri)?; + run(async move { + let c = connect(&rpc)?; + let nonce = rpc_next_nonce(&c, &account).await?; + let genesis = rpc_genesis_hash(&c).await?; + let xt = build_signed(call, &pair, &account, nonce, genesis)?; + let hash = rpc_submit(&c, &xt).await?; + println!("Submitted {what}"); + println!(" signer: {}", account.to_ss58check()); + println!(" nonce: {nonce}"); + println!(" extrinsic hash: {hash:?}"); + println!(" The node has accepted it into the pool; watch its logs for block inclusion."); + Ok(()) + }) +} + +/// Sign + submit `ghostConsensus.stake(amount)`. +pub fn stake(amount: u128, suri: &str, rpc: Option<&str>) -> Result<(), String> { + let call = RuntimeCall::GhostConsensus(pallet_ghost_consensus::Call::stake { amount }); + submit_call(call, suri, rpc, &format!("stake({amount})")) +} + +/// Sign + submit `ghostConsensus.unstake(amount)`. +pub fn unstake(amount: u128, suri: &str, rpc: Option<&str>) -> Result<(), String> { + let call = RuntimeCall::GhostConsensus(pallet_ghost_consensus::Call::unstake { amount }); + submit_call(call, suri, rpc, &format!("unstake({amount})")) +} + +/// Sign + submit `balances.transfer_keep_alive(dest, amount)`. +pub fn transfer(dest: &str, amount: u128, suri: &str, rpc: Option<&str>) -> Result<(), String> { + let dest_acc = resolve_account(dest)?; + let call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { + dest: MultiAddress::Id(dest_acc.clone()), + value: amount, + }); + submit_call( + call, + suri, + rpc, + &format!("transfer({amount}) to {}", dest_acc.to_ss58check()), + ) +} + +/// Generate an ML-DSA-87 (FIPS 204 / "Dilithium-5") keypair and write it to +/// `<out>.pub` (2592 bytes) and `<out>.sec` (4896 bytes). +pub fn generate_ml_dsa_key(out_prefix: &str) -> Result<(), String> { + // ML-DSA-87 keygen expands a large matrix in big stack buffers, which can overflow the + // main thread's stack. Run it on a worker thread with a generous stack. + let (pk, sk) = std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(ml_dsa_87::try_keygen) + .map_err(|e| format!("failed to spawn keygen thread: {e}"))? + .join() + .map_err(|_| "keygen thread panicked".to_string())? + .map_err(|e| format!("ML-DSA keygen failed: {e}"))?; + let pub_path = format!("{out_prefix}.pub"); + let sec_path = format!("{out_prefix}.sec"); + std::fs::write(&pub_path, pk.into_bytes()).map_err(|e| format!("write {pub_path}: {e}"))?; + std::fs::write(&sec_path, sk.into_bytes()).map_err(|e| format!("write {sec_path}: {e}"))?; + println!("Generated ML-DSA-87 (Dilithium-5, FIPS 204) keypair:"); + println!(" public key: {pub_path} (2592 bytes)"); + println!(" secret key: {sec_path} (4896 bytes) [keep this file secret]"); + println!("\nRegister it on-chain as a validator attestation key with:"); + println!(" ghost register-key --key {pub_path} --account <your-secret-uri>"); + Ok(()) +} + +/// Read an ML-DSA-87 public key file and submit `ghostConsensus.register_ml_dsa_key`. +/// Once registered, the signer's `validate_block` attestations and `verify_pq_signature` +/// calls are checked on-chain against this key. +pub fn register_ml_dsa_key(key_path: &str, suri: &str, rpc: Option<&str>) -> Result<(), String> { + let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read '{key_path}': {e}"))?; + if bytes.len() != 2592 { + return Err(format!( + "ML-DSA-87 public key must be exactly 2592 bytes, but '{key_path}' has {}", + bytes.len() + )); + } + let public_key = bytes + .try_into() + .map_err(|_| "public key exceeds the 2592-byte bound".to_string())?; + let call = RuntimeCall::GhostConsensus(pallet_ghost_consensus::Call::register_ml_dsa_key { + algorithm: pallet_ghost_consensus::types::PqAlgorithm::MlDsa87, + public_key, + }); + submit_call(call, suri, rpc, "register_ml_dsa_key(ML-DSA-87)") +} diff --git a/pallets/pallet-ghost-consensus/Cargo.toml b/pallets/pallet-ghost-consensus/Cargo.toml index 7d90225..0b1b981 100644 --- a/pallets/pallet-ghost-consensus/Cargo.toml +++ b/pallets/pallet-ghost-consensus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-ghost-consensus" -description = "Hybrid Proof-of-Work and Proof-of-Stake consensus pallet for Ghost blockchain." +description = "Experimental Ghost PoW/PoS runtime pallet." version = "0.0.0" license = "Unlicense" authors.workspace = true @@ -16,28 +16,39 @@ targets = ["x86_64-unknown-linux-gnu"] workspace = true [dependencies] -codec = { features = ["derive"], workspace = true } -scale-info = { features = ["derive"], workspace = true } - -# frame deps -frame-benchmarking = { optional = true, workspace = true } +codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +serde = { workspace = true, optional = true, features = ["derive"] } +frame-benchmarking = { workspace = true, optional = true } frame-support = { workspace = true } frame-system = { workspace = true } - -# Additional deps for consensus +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } sp-core = { workspace = true } sp-runtime = { workspace = true } -pallet-balances = { workspace = true } -# Hash functions -sp-crypto-hashing = { workspace = true } -pqcrypto-dilithium = { version = "0.5.0", default-features = false } -pqcrypto-traits = { version = "0.3.5", default-features = false } +# Post-quantum signatures: ML-DSA / NIST FIPS 204 ("Dilithium" levels 2/3/5). +# Pure-Rust, no_std, allocation-free verify path. `default-rng` (the only thing +# that pulls getrandom) is intentionally NOT enabled here — the runtime only ever +# verifies signatures, so this compiles cleanly into the Wasm runtime. +fips204 = { version = "0.4.6", default-features = false, features = [ + "ml-dsa-44", + "ml-dsa-65", + "ml-dsa-87", +] } [dev-dependencies] sp-core = { workspace = true, default-features = true } sp-io = { workspace = true, default-features = true } sp-runtime = { workspace = true, default-features = true } +# Enable RNG-backed keygen/signing for tests only. Feature unification means the +# test build gets `default-rng`; the runtime (non-test) build never does. +fips204 = { version = "0.4.6", default-features = false, features = [ + "ml-dsa-44", + "ml-dsa-65", + "ml-dsa-87", + "default-rng", +] } [features] default = ["std"] @@ -46,23 +57,24 @@ std = [ "frame-benchmarking?/std", "frame-support/std", "frame-system/std", + "pallet-balances/std", + "pallet-timestamp/std", "scale-info/std", + "serde/std", "sp-core/std", - # "sp-consensus/std", # REMOVE: sp-consensus does not have std feature - "sp-crypto-hashing/std", "sp-io/std", "sp-runtime/std", - # "sp-blockchain/std", # REMOVE: sp-blockchain does not have std feature - "pallet-balances/std", - # "sc-client-api/std" # REMOVE: sc-client-api does not have std feature - "pqcrypto-dilithium/std", - "pqcrypto-traits/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-timestamp/try-runtime", ] diff --git a/pallets/pallet-ghost-consensus/src/benchmarking.rs b/pallets/pallet-ghost-consensus/src/benchmarking.rs new file mode 100644 index 0000000..7bf5ad5 --- /dev/null +++ b/pallets/pallet-ghost-consensus/src/benchmarking.rs @@ -0,0 +1,255 @@ +//! Benchmark coverage for the Ghost consensus pallet. +//! +//! This module focuses on the hardened dispatchables that exist today and keeps +//! PQ readiness fixtures close at hand so future registry/attestation calls can +//! reuse the same setup without reshaping benchmark inputs. + +use super::*; +use crate::functions::{ + validate_pq_proof_envelope, validate_pq_readiness_metadata, verify_pow_enhanced, +}; +use crate::types::{ + DefaultPqProof, GhostBlockHeader, MisbehaviorEvidence, PqAlgorithm, PqProofKind, + PqReadinessMetadata, SlashingReason, +}; +use frame_benchmarking::{account, v2::*}; +use frame_support::{assert_ok, BoundedVec}; +use frame_system::RawOrigin; +use sp_core::H256; +use sp_runtime::traits::{BlakeTwo256, Hash, Saturating, Zero}; + +const SEED: u32 = 0; +const MAX_NONCE_SEARCH: u64 = 10_000; + +fn repeated_min_stake<T: Config>(multiplier: u32) -> BalanceOf<T> { + let mut total: BalanceOf<T> = Zero::zero(); + for _ in 0..multiplier { + total = total.saturating_add(T::MinStake::get()); + } + total +} + +fn fund_account<T: Config>(account: &T::AccountId, amount: BalanceOf<T>) { + let _ = pallet_balances::Pallet::<T>::deposit_creating(account, amount); +} + +fn genesis_header() -> GhostBlockHeader { + GhostBlockHeader { + number: 0, + parent_hash: H256::zero(), + state_root: BlakeTwo256::hash_of(&(0u32, "state")), + extrinsics_root: BlakeTwo256::hash_of(&(0u32, "extrinsics")), + nonce: 0, + difficulty: 1_000_000_000_000, + validator_signature: None, + } +} + +fn mine_header<T: Config>(number: u32, parent: &GhostBlockHeader) -> GhostBlockHeader { + // Conventional difficulty: higher = harder. 16 keeps the PoW search cheap. + Difficulty::<T>::put(16); + + let mut header = GhostBlockHeader { + number, + parent_hash: BlakeTwo256::hash_of(parent), + state_root: BlakeTwo256::hash_of(&(number, "state")), + extrinsics_root: BlakeTwo256::hash_of(&(number, "extrinsics")), + nonce: 0, + difficulty: Difficulty::<T>::get(), + validator_signature: None, + }; + + for nonce in 0..MAX_NONCE_SEARCH { + header.nonce = nonce; + if verify_pow_enhanced(&header, header.difficulty) { + return header; + } + } + + panic!("failed to find benchmark nonce"); +} + +fn setup_validator<T: Config>(name: &'static str, index: u32) -> T::AccountId { + let validator = account(name, index, SEED); + let endowment = repeated_min_stake::<T>(8); + let stake = repeated_min_stake::<T>(2); + fund_account::<T>(&validator, endowment); + assert_ok!(Pallet::<T>::stake( + RawOrigin::Signed(validator.clone()).into(), + stake + )); + validator +} + +fn sample_pq_metadata<T: Config>() -> PqReadinessMetadata<BlockNumberFor<T>> { + let now = frame_system::Pallet::<T>::block_number(); + + PqReadinessMetadata { + version: 1, + algorithm: PqAlgorithm::MlDsa65, + proof_kind: PqProofKind::Attestation, + key_strength_bits: 192, + claimed_nist_level: Some(3), + issued_at: Some(now), + expires_at: Some(now.saturating_add(10u32.into())), + public_key_commitment: H256::repeat_byte(0xAB), + metadata_hash: Some(H256::repeat_byte(0xCD)), + flags: 0b0000_0011, + } +} + +fn sample_pq_proof<T: Config>() -> DefaultPqProof<BlockNumberFor<T>> { + DefaultPqProof { + algorithm: PqAlgorithm::MlDsa65, + proof_kind: PqProofKind::Attestation, + submitted_at: frame_system::Pallet::<T>::block_number().saturating_add(1u32.into()), + statement_hash: H256::repeat_byte(0x11), + public_key_commitment: H256::repeat_byte(0xAB), + proof: BoundedVec::try_from(vec![7u8; 96]).expect("proof fits benchmark bound"), + context: BoundedVec::try_from(b"ghost-pq-attestation".to_vec()) + .expect("context fits benchmark bound"), + auxiliary_hash: Some(H256::repeat_byte(0x22)), + } +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn stake() { + let caller: T::AccountId = account("staker", 0, SEED); + let amount = repeated_min_stake::<T>(2); + fund_account::<T>(&caller, repeated_min_stake::<T>(8)); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), amount); + + assert_eq!(ValidatorStakes::<T>::get(&caller), Some(amount)); + assert_eq!(ValidatorCount::<T>::get(), 1); + } + + #[benchmark] + fn unstake() { + let caller = setup_validator::<T>("staker", 1); + let amount = repeated_min_stake::<T>(1); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), amount); + + assert_eq!(ValidatorStakes::<T>::get(&caller), Some(amount)); + } + + #[benchmark] + fn submit_block() { + let miner: T::AccountId = account("miner", 0, SEED); + let parent = genesis_header(); + BlockHeaders::<T>::insert(0, parent.clone()); + let header = mine_header::<T>(1, &parent); + + #[extrinsic_call] + _(RawOrigin::Signed(miner.clone()), header.clone()); + + assert_eq!(CurrentPhase::<T>::get(), ConsensusPhase::PosValidation); + assert_eq!(PendingValidationBlock::<T>::get(), Some(header.number)); + assert_eq!(BlockMiners::<T>::get(header.number), Some(miner)); + } + + #[benchmark] + fn validate_block() { + let miner: T::AccountId = account("miner", 1, SEED); + let validator = setup_validator::<T>("validator", 0); + let parent = genesis_header(); + BlockHeaders::<T>::insert(0, parent.clone()); + let header = mine_header::<T>(1, &parent); + assert_ok!(Pallet::<T>::submit_block( + RawOrigin::Signed(miner.clone()).into(), + header + )); + + #[extrinsic_call] + _(RawOrigin::Signed(validator.clone()), 1u32, None); + + let stored_header = BlockHeaders::<T>::get(1).expect("validated block exists"); + assert!(stored_header.validator_signature.is_some()); + assert_eq!(CurrentPhase::<T>::get(), ConsensusPhase::Finalization); + } + + #[benchmark] + fn report_misbehavior() { + let reporter: T::AccountId = account("reporter", 0, SEED); + let validator = setup_validator::<T>("validator", 2); + fund_account::<T>(&reporter, repeated_min_stake::<T>(2)); + + #[extrinsic_call] + _(RawOrigin::Signed(reporter), validator.clone(), SlashingReason::DoubleSigning, MisbehaviorEvidence::DoubleSigning { + first_vote: H256::repeat_byte(0x01), + second_vote: H256::repeat_byte(0x02), + }); + + assert!(DoubleSignReports::<T>::get(&validator)); + assert_eq!(SlashingRecords::<T>::get().len(), 1); + } + + #[benchmark] + fn register_pq_readiness() { + let caller: T::AccountId = account("pq-account", 0, SEED); + let metadata = sample_pq_metadata::<T>(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), metadata.clone()); + + assert_eq!(PqReadinessRegistry::<T>::get(&caller), Some(metadata)); + } + + #[benchmark] + fn attest_pq_readiness() { + let caller: T::AccountId = account("pq-account", 1, SEED); + let metadata = sample_pq_metadata::<T>(); + let proof = sample_pq_proof::<T>(); + + assert_ok!(Pallet::<T>::register_pq_readiness( + RawOrigin::Signed(caller.clone()).into(), + metadata, + )); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), proof.clone()); + + assert_eq!(PqReadinessAttestations::<T>::get(&caller), Some(proof)); + } + + #[benchmark] + fn remove_pq_readiness() { + let caller: T::AccountId = account("pq-account", 2, SEED); + let metadata = sample_pq_metadata::<T>(); + let proof = sample_pq_proof::<T>(); + + assert_ok!(Pallet::<T>::register_pq_readiness( + RawOrigin::Signed(caller.clone()).into(), + metadata, + )); + assert_ok!(Pallet::<T>::attest_pq_readiness( + RawOrigin::Signed(caller.clone()).into(), + proof, + )); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone())); + + assert!(!PqReadinessRegistry::<T>::contains_key(&caller)); + assert!(!PqReadinessAttestations::<T>::contains_key(&caller)); + } + + #[benchmark] + fn pq_readiness_attestation_fixture() { + let metadata = sample_pq_metadata::<T>(); + let proof = sample_pq_proof::<T>(); + + #[block] + { + assert_ok!(validate_pq_readiness_metadata::<T>(&metadata)); + assert_ok!(validate_pq_proof_envelope::<T>(&metadata, &proof)); + } + } +} diff --git a/pallets/pallet-ghost-consensus/src/consensus.rs b/pallets/pallet-ghost-consensus/src/consensus.rs deleted file mode 100644 index cda54c0..0000000 --- a/pallets/pallet-ghost-consensus/src/consensus.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Ghost Consensus Engine Implementation - -use super::*; -use crate::types::*; -use frame_support::pallet_prelude::*; -use sp_consensus::{BlockImport, Environment, Proposer, SelectChain, ForkChoiceStrategy}; -use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor}; -use std::sync::Arc; -use sc_client_api::Backend; -use sp_blockchain::HeaderBackend; - -/// Ghost Consensus Engine -pub struct GhostConsensusEngine<B, C> { - client: Arc<C>, - _phantom: sp_std::marker::PhantomData<B>, -} - -impl<B, C> GhostConsensusEngine<B, C> { - /// Create a new Ghost consensus engine - pub fn new(client: Arc<C>) -> Self { - Self { - client, - _phantom: Default::default(), - } - } -} - -impl<B, C> sp_consensus::ConsensusEngine for GhostConsensusEngine<B, C> -where - B: BlockT, - C: HeaderBackend<B> + 'static, -{ - type Block = B; - - fn name(&self) -> &str { - "GhostConsensus" - } - - fn version(&self) -> sp_consensus::ConsensusEngineVersion { - sp_consensus::ConsensusEngineVersion::new(1, 0, 0) - } - - fn fork_choice_strategy(&self) -> ForkChoiceStrategy { - ForkChoiceStrategy::LongestChain - } - - fn block_import(&self) -> Box<dyn BlockImport<B>> { - Box::new(GhostBlockImport::new(self.client.clone())) - } -} - -/// Ghost Block Import -pub struct GhostBlockImport<B, C> { - client: Arc<C>, - _phantom: sp_std::marker::PhantomData<B>, -} - -impl<B, C> GhostBlockImport<B, C> { - pub fn new(client: Arc<C>) -> Self { - Self { - client, - _phantom: Default::default(), - } - } -} - -impl<B, C> BlockImport<B> for GhostBlockImport<B, C> -where - B: BlockT, - C: HeaderBackend<B> + 'static, -{ - type Error = sp_consensus::Error; - - fn check_block( - &mut self, - block: sp_consensus::BlockCheckParams<B>, - ) -> Result<sp_consensus::ImportResult, Self::Error> { - // Basic block validation - // In a full implementation, this would validate the Ghost consensus rules - - Ok(sp_consensus::ImportResult::Imported(sp_consensus::ImportedAux { - header_only: false, - clear_justification_requests: false, - needs_justification: false, - bad_justification: false, - is_new_best: true, - })) - } - - fn import_block( - &mut self, - block: sp_consensus::BlockImportParams<B>, - ) -> Result<sp_consensus::ImportResult, Self::Error> { - // Import the block - // In a full implementation, this would handle the Ghost consensus import logic - - Ok(sp_consensus::ImportResult::Imported(sp_consensus::ImportedAux { - header_only: false, - clear_justification_requests: false, - needs_justification: false, - bad_justification: false, - is_new_best: true, - })) - } -} - -/// Ghost Proposer for block production -pub struct GhostProposer<B, C> { - client: Arc<C>, - _phantom: sp_std::marker::PhantomData<B>, -} - -impl<B, C> GhostProposer<B, C> { - pub fn new(client: Arc<C>) -> Self { - Self { - client, - _phantom: Default::default(), - } - } -} - -impl<B, C> Proposer<B> for GhostProposer<B, C> -where - B: BlockT, - C: HeaderBackend<B> + 'static, -{ - type Error = sp_consensus::Error; - type Proposal = sp_consensus::Proposal<B, sp_consensus::TransactionFor<C, B>>; - type ProofRecording = sp_consensus::EnableProofRecording; - type Proof = sp_consensus::ConsensusEngineProof; - - fn propose( - &mut self, - _: sp_consensus::ProposalParameters, - ) -> Result<Self::Proposal, Self::Error> { - // Create a block proposal - // In a full implementation, this would create blocks according to Ghost rules - - Err(sp_consensus::Error::CannotPropose.into()) - } -} - -/// Mining worker for PoW -pub struct GhostMiningWorker { - difficulty: u64, -} - -impl GhostMiningWorker { - pub fn new(difficulty: u64) -> Self { - Self { difficulty } - } - - /// Mine a block with the given parameters - pub fn mine_block(&self, block_header: &GhostBlockHeader) -> Option<u64> { - use crate::functions::verify_pow_enhanced; - - let mut nonce = 0u64; - - // Simple mining loop (in production, this would be optimized) - for _ in 0..1_000_000 { // Limit attempts to prevent infinite loop - let mut test_header = block_header.clone(); - test_header.nonce = nonce; - - if verify_pow_enhanced(&test_header, self.difficulty) { - return Some(nonce); - } - - nonce += 1; - } - - None // No valid nonce found - } -} diff --git a/pallets/pallet-ghost-consensus/src/functions.rs b/pallets/pallet-ghost-consensus/src/functions.rs index 14fc95d..aba3ea8 100644 --- a/pallets/pallet-ghost-consensus/src/functions.rs +++ b/pallets/pallet-ghost-consensus/src/functions.rs @@ -1,305 +1,400 @@ -//! Core functions for Ghost Consensus +//! Pure helper functions used by the Ghost consensus pallet. use super::*; -use crate::types::*; -use frame_support::{pallet_prelude::*, traits::Currency}; -use sp_core::H256; -use sp_runtime::traits::{BlakeTwo256, Hash, SaturatedConversion, Zero}; -use sp_runtime::sp_std::collections::btree_map::BTreeMap; +use crate::types::{ + BlockReward, DefaultPqProof, GhostBlockHeader, MisbehaviorEvidence, PosSelection, PqAlgorithm, + PqProofKind, PqReadinessMetadata, SlashingReason, ValidatorStake, +}; +use frame_support::pallet_prelude::*; +use sp_core::{H256, U256}; +use sp_runtime::traits::{BlakeTwo256, Hash, SaturatedConversion, Saturating, Zero}; + +fn is_structurally_known_pq_algorithm(algorithm: &PqAlgorithm) -> bool { + match algorithm { + PqAlgorithm::Unknown => false, + PqAlgorithm::Other(label) => label.iter().any(|byte| *byte != 0), + _ => true, + } +} + +fn is_structurally_known_pq_proof_kind(proof_kind: &PqProofKind) -> bool { + !matches!(proof_kind, PqProofKind::Unknown) +} -/// Calculate mining difficulty adjustment +/// Calculate a simple difficulty adjustment towards the target block time. pub fn calculate_difficulty_adjustment<T: Config>( - current_difficulty: u64, - actual_block_time: u64, - target_block_time: u64, - entropy: u64, + current_difficulty: u64, + actual_block_time: u64, + target_block_time: u64, ) -> u64 { - let mut adjustment_factor = if actual_block_time < target_block_time { - // Increase difficulty if blocks are too fast - (target_block_time * 100) / actual_block_time - } else { - // Decrease difficulty if blocks are too slow - (actual_block_time * 100) / target_block_time - }; - - // Entropy Steering: - // Maximum entropy for N items is log2(N). - // For N=100, max entropy is ~6.64, scaled: 6,640,000. - // If entropy is low (centralization), we increase difficulty significantly. - // We'll use 4,000,000 (log2(16)) as a "healthy decentralization" threshold. - let entropy_threshold = 4_000_000; - if entropy < entropy_threshold { - // Increase difficulty multiplier if entropy is low. - // multiplier = 1 + (threshold - entropy) / threshold - // We scale it by 100 for our adjustment_factor. - let entropy_penalty = ((entropy_threshold - entropy) * 100) / entropy_threshold; - adjustment_factor = adjustment_factor.saturating_add(entropy_penalty); - } - - (current_difficulty * adjustment_factor) / 100 + if actual_block_time == 0 { + return current_difficulty; + } + + current_difficulty + .saturating_mul(target_block_time) + .checked_div(actual_block_time) + .unwrap_or(current_difficulty) } -/// Verify Proof-of-Work using Blake2-256 (ASIC-resistant, fast for 5-second blocks) -pub fn verify_pow(block_header: &GhostBlockHeader, target_difficulty: u64) -> bool { - let hash_input = ( - block_header.number, - block_header.parent_hash, - block_header.state_root, - block_header.extrinsics_root, - block_header.nonce, - ); - - let hash = BlakeTwo256::hash_of(&hash_input); - let hash_value = u64::from_be_bytes(hash.as_bytes()[0..8].try_into().unwrap_or_default()); - - hash_value <= target_difficulty +/// Whether `hash` satisfies the conventional difficulty `difficulty`. +/// +/// Difficulty uses the standard convention: numerically *larger* difficulty is +/// *harder*. The hash is interpreted as a big-endian 256-bit integer and is valid +/// iff `hash <= U256::MAX / difficulty`. The full 256 bits are compared (no +/// truncation), and `difficulty` is clamped to a minimum of 1 to avoid division +/// by zero (difficulty 1 = easiest, every hash passes). +pub fn meets_difficulty(hash: &H256, difficulty: u64) -> bool { + let work = U256::from_big_endian(hash.as_bytes()); + let target = U256::MAX / U256::from(difficulty.max(1)); + work <= target } -/// Enhanced Blake2 PoW with additional ASIC resistance -pub fn verify_pow_enhanced(block_header: &GhostBlockHeader, target_difficulty: u64) -> bool { - // Double hash for additional ASIC resistance - let hash_input = ( - block_header.number, - block_header.parent_hash, - block_header.state_root, - block_header.extrinsics_root, - block_header.nonce, - ); - - let first_hash = BlakeTwo256::hash_of(&hash_input); - let final_hash = BlakeTwo256::hash_of(&first_hash); - let hash_value = u64::from_be_bytes(final_hash.as_bytes()[0..8].try_into().unwrap_or_default()); - - hash_value <= target_difficulty +/// Double Blake2-256 work hash over the header's PoW preimage. This is the +/// canonical Ghost PoW hash and matches the node's miner. +pub fn pow_work_hash(block_header: &GhostBlockHeader) -> H256 { + let hash_input = ( + block_header.number, + block_header.parent_hash, + block_header.state_root, + block_header.extrinsics_root, + block_header.nonce, + ); + let first_hash = BlakeTwo256::hash_of(&hash_input); + BlakeTwo256::hash_of(&first_hash) } -/// Alternative: Verify Proof-of-Work using double SHA-256 (Bitcoin-style) -pub fn verify_pow_sha256(block_header: &GhostBlockHeader, target_difficulty: u64) -> bool { - use sp_core::sha2_256; - - let hash_input = ( - block_header.number, - block_header.parent_hash, - block_header.state_root, - block_header.extrinsics_root, - block_header.nonce, - ); - - // First SHA-256 - let first_hash = sha2_256(&hash_input.encode()); - - // Second SHA-256 (Bitcoin standard) - let final_hash = sha2_256(&first_hash); - - // Convert first 8 bytes to u64 for difficulty comparison - let hash_value = u64::from_be_bytes(final_hash[0..8].try_into().unwrap_or_default()); +/// Verify Proof-of-Work using a single Blake2-256 hash. +pub fn verify_pow(block_header: &GhostBlockHeader, difficulty: u64) -> bool { + let hash_input = ( + block_header.number, + block_header.parent_hash, + block_header.state_root, + block_header.extrinsics_root, + block_header.nonce, + ); + meets_difficulty(&BlakeTwo256::hash_of(&hash_input), difficulty) +} - hash_value <= target_difficulty +/// Verify Proof-of-Work using a double Blake2-256 hash (the canonical Ghost PoW). +pub fn verify_pow_enhanced(block_header: &GhostBlockHeader, difficulty: u64) -> bool { + meets_difficulty(&pow_work_hash(block_header), difficulty) } -/// Alternative: Verify Proof-of-Work using Keccak-256 (Ethereum-style) -pub fn verify_pow_keccak(block_header: &GhostBlockHeader, target_difficulty: u64) -> bool { - use sp_core::keccak_256; +/// Verify Proof-of-Work using double SHA-256. +pub fn verify_pow_sha256(block_header: &GhostBlockHeader, difficulty: u64) -> bool { + use sp_core::sha2_256; + + let hash_input = ( + block_header.number, + block_header.parent_hash, + block_header.state_root, + block_header.extrinsics_root, + block_header.nonce, + ); + + let first_hash = sha2_256(&hash_input.encode()); + let final_hash = sha2_256(&first_hash); + meets_difficulty(&H256::from_slice(&final_hash), difficulty) +} - let hash_input = ( - block_header.number, - block_header.parent_hash, - block_header.state_root, - block_header.extrinsics_root, - block_header.nonce, - ); +/// Verify Proof-of-Work using Keccak-256. +pub fn verify_pow_keccak(block_header: &GhostBlockHeader, difficulty: u64) -> bool { + use sp_core::keccak_256; - let hash = keccak_256(&hash_input.encode()); - let hash_value = u64::from_be_bytes(hash[0..8].try_into().unwrap_or_default()); + let hash_input = ( + block_header.number, + block_header.parent_hash, + block_header.state_root, + block_header.extrinsics_root, + block_header.nonce, + ); - hash_value <= target_difficulty + meets_difficulty(&H256::from_slice(&keccak_256(&hash_input.encode())), difficulty) } -/// Select PoS validator based on stake weight +/// Select a validator using stake-weighted sampling. pub fn select_pos_validator<T: Config>( - stakers: Vec<ValidatorStake<T::AccountId, BalanceOf<T>>>, - seed: H256, + stakers: Vec<ValidatorStake<T::AccountId, BalanceOf<T>>>, + seed: H256, ) -> Option<PosSelection<T::AccountId>> { - if stakers.is_empty() { - return None; - } - - let total_weight: u64 = stakers.iter().map(|s| s.weight).sum(); - if total_weight == 0 { - return None; - } - - let mut random_value = u64::from_be_bytes(seed.as_bytes()[0..8].try_into().unwrap_or_default()); - random_value %= total_weight; - - let mut cumulative_weight = 0u64; - for staker in stakers { - cumulative_weight += staker.weight; - if random_value < cumulative_weight { - return Some(PosSelection { - validator: staker.account, - weight: staker.weight, - round: frame_system::Pallet::<T>::block_number().saturated_into(), - }); - } - } - - None + if stakers.is_empty() { + return None; + } + + // Saturating accumulation: weights are u64 and a naive `sum()`/`+=` would + // overflow (and silently wrap in release) for large validator sets. + let total_weight: u64 = stakers + .iter() + .fold(0u64, |acc, stake| acc.saturating_add(stake.weight)); + if total_weight == 0 { + return None; + } + + let random_value = + u64::from_be_bytes(seed.as_bytes()[0..8].try_into().unwrap_or_default()) % total_weight; + + let mut cumulative_weight = 0u64; + for staker in stakers { + cumulative_weight = cumulative_weight.saturating_add(staker.weight); + if random_value < cumulative_weight { + return Some(PosSelection { + validator: staker.account, + weight: staker.weight, + round: frame_system::Pallet::<T>::block_number().saturated_into::<u64>(), + }); + } + } + + None } -/// Calculate block rewards +/// Split a block reward 40/60 between miner and stakers. pub fn calculate_block_reward<T: Config>(total_reward: BalanceOf<T>) -> BlockReward<BalanceOf<T>> { - let miner_reward = (total_reward * 40u32.into()) / 100u32.into(); // 40% - let stakers_reward = total_reward - miner_reward; // 60% - - BlockReward { - total: total_reward, - miner_reward, - stakers_reward, - } + // Saturating arithmetic: a chain must never panic (debug) or silently wrap + // (release) on a large block reward. 40% to the miner, 60% to stakers. + let miner_reward = total_reward.saturating_mul(40u32.into()) / 100u32.into(); + let stakers_reward = total_reward.saturating_sub(miner_reward); + + BlockReward { + total: total_reward, + miner_reward, + stakers_reward, + } } -/// Validate block header +/// Validate a submitted Ghost block header against its parent. pub fn validate_block_header<T: Config>( - header: &GhostBlockHeader, - parent_header: &GhostBlockHeader, + header: &GhostBlockHeader, + parent_header: &GhostBlockHeader, ) -> DispatchResult { - // Check block number sequence - ensure!(header.number == parent_header.number + 1, Error::<T>::InvalidBlockNumber); - - // Check parent hash - ensure!(header.parent_hash == BlakeTwo256::hash_of(parent_header), Error::<T>::InvalidParentHash); - - // Verify PoW (using enhanced Blake2 for better ASIC resistance) - ensure!(verify_pow_enhanced(header, header.difficulty), Error::<T>::InvalidPow); - - // Check difficulty is reasonable - let expected_difficulty = Difficulty::<T>::get(); - ensure!(header.difficulty >= expected_difficulty / 2, Error::<T>::DifficultyTooLow); - ensure!(header.difficulty <= expected_difficulty * 2, Error::<T>::DifficultyTooHigh); - - Ok(()) + // Checked successor: prevents a u32 wrap (parent.number == u32::MAX) from + // producing header.number == 0 and overwriting the genesis header. + ensure!( + parent_header.number.checked_add(1) == Some(header.number), + Error::<T>::InvalidBlockNumber + ); + ensure!( + header.parent_hash == BlakeTwo256::hash_of(parent_header), + Error::<T>::InvalidParentHash + ); + + // PoW MUST be verified against the canonical on-chain difficulty, never the + // attacker-supplied `header.difficulty`. The header must also declare exactly + // the current difficulty, so a miner cannot mine against a stale/easier target. + let expected_difficulty = Difficulty::<T>::get(); + ensure!( + header.difficulty == expected_difficulty, + Error::<T>::DifficultyMismatch + ); + ensure!( + verify_pow_enhanced(header, expected_difficulty), + Error::<T>::InvalidPow + ); + + Ok(()) } -/// Distribute rewards to miner and stakers -pub fn distribute_rewards<T: Config>( - miner: T::AccountId, - stakers: Vec<ValidatorStake<T::AccountId, BalanceOf<T>>>, - reward: BlockReward<BalanceOf<T>>, +/// Validate slashing evidence before stake can be reduced. +pub fn validate_misbehavior_evidence<T: Config>( + validator: &T::AccountId, + reason: &SlashingReason, + evidence: &MisbehaviorEvidence, ) -> DispatchResult { - // Reward the miner - let _ = pallet_balances::Pallet::<T>::deposit_creating(&miner, reward.miner_reward); - - // Distribute to stakers proportionally - let total_stake: BalanceOf<T> = stakers.iter().fold(Zero::zero(), |acc, s| acc + s.stake); - if !total_stake.is_zero() { - for staker in stakers { - let staker_reward = (reward.stakers_reward * staker.stake) / total_stake; - let _ = pallet_balances::Pallet::<T>::deposit_creating(&staker.account, staker_reward); - } - } - - Ok(()) + match (reason, evidence) { + ( + SlashingReason::DoubleSigning, + MisbehaviorEvidence::DoubleSigning { + first_vote, + second_vote, + }, + ) => { + ensure!(first_vote != second_vote, Error::<T>::InvalidEvidence); + ensure!(*first_vote != H256::zero(), Error::<T>::InvalidEvidence); + ensure!(*second_vote != H256::zero(), Error::<T>::InvalidEvidence); + ensure!( + !DoubleSignReports::<T>::get(validator), + Error::<T>::InvalidEvidence + ); + Ok(()) + } + (SlashingReason::InvalidBlock, MisbehaviorEvidence::InvalidBlock { block_number }) => { + let header = BlockHeaders::<T>::get(*block_number).ok_or(Error::<T>::BlockNotFound)?; + + // Evidence is valid iff the block's PoW does NOT satisfy the difficulty the + // header itself claims, AND the accused validator is the recorded validator. + // We verify against the header's OWN difficulty (not the current on-chain + // difficulty) so a later retarget cannot retroactively make a correctly + // validated block look slashable. + ensure!( + !verify_pow_enhanced(&header, header.difficulty), + Error::<T>::InvalidEvidence + ); + ensure!( + BlockValidators::<T>::get(*block_number).as_ref() == Some(validator), + Error::<T>::InvalidEvidence + ); + ensure!( + !InvalidBlockReports::<T>::get(validator), + Error::<T>::InvalidEvidence + ); + Ok(()) + } + (SlashingReason::Downtime, MisbehaviorEvidence::Downtime) => { + let current_block = frame_system::Pallet::<T>::block_number().saturated_into::<u32>(); + let last_active = LastActiveBlock::<T>::get(validator); + ensure!( + current_block.saturating_sub(last_active) > T::MaxDowntimeBlocks::get(), + Error::<T>::InvalidEvidence + ); + Ok(()) + } + (SlashingReason::Other, MisbehaviorEvidence::Other { proof_hash }) => { + ensure!(*proof_hash != H256::zero(), Error::<T>::InvalidEvidence); + Ok(()) + } + _ => Err(Error::<T>::InvalidEvidence.into()), + } } -/// Calculate Shannon entropy for a list of accounts. -/// Used to measure centralization in the block production process. -/// Returns entropy scaled by 1,000,000. +/// Validate claimed PQ metadata as a structural registry claim only. /// -/// H = -sum(pi * log2(pi)) -/// -/// We use scaled integers to avoid floats. -/// p_i = count_i / total_count -/// log2(p_i) = log2(count_i) - log2(total_count) -pub fn calculate_entropy<T: Config>(producers: Vec<T::AccountId>) -> u64 { - let total_count = producers.len() as u64; - if total_count == 0 { - return 0; - } - - // Count occurrences of each producer - let mut counts = BTreeMap::new(); - for producer in producers { - *counts.entry(producer).or_insert(0u64) += 1; - } - - let mut total_entropy: u64 = 0; - let log2_total = integer_log2_scaled(total_count); - - for count in counts.values() { - // p_i = count / total_count - // Entropy contribution = -(p_i * log2(p_i)) - // In scaled terms: - (count / total_count) * (log2(count) - log2(total_count)) - // => (count * (log2(total_count) - log2(count))) / total_count - - let log2_count = integer_log2_scaled(*count); - let term = (*count * (log2_total.saturating_sub(log2_count))) / total_count; - total_entropy = total_entropy.saturating_add(term); - } - - total_entropy -} - -/// Deterministic integer-based log2 scaled by 1,000,000. -/// Uses the property: log2(x) = log2(x * 2^k) - k -/// For higher precision, we scale the input. -fn integer_log2_scaled(x: u64) -> u64 { - if x == 0 { return 0; } - - let scaling_factor = 1_000_000; - - // log2(x) * 1,000,000 - // We use the leading zeros to find the integer part. - let leading_zeros = x.leading_zeros(); - let integer_part = (63 - leading_zeros) as u64; - - // Fractional part approximation: log2(x) approx integer_part + (x / 2^integer_part) - 1 - // More accurately using bit shifts for (x / 2^integer_part): - let rem = x ^ (1 << integer_part); - let fractional_part = (rem * scaling_factor) / (1 << integer_part); - - (integer_part * scaling_factor) + fractional_part -} - -/// Check for slashing conditions -pub fn check_slashing_conditions<T: Config>( - _validator: T::AccountId, -) -> Option<SlashingReason> { - // This function would need access to storage, so it's better implemented in the pallet - // For now, return None - None +/// This does not perform cryptographic verification of PQ keys, proofs, or signatures. +pub fn validate_pq_readiness_metadata<T: Config>( + metadata: &PqReadinessMetadata<BlockNumberFor<T>>, +) -> DispatchResult { + ensure!(metadata.version > 0, Error::<T>::InvalidPqMetadata); + ensure!( + is_structurally_known_pq_algorithm(&metadata.algorithm), + Error::<T>::InvalidPqMetadata + ); + ensure!( + is_structurally_known_pq_proof_kind(&metadata.proof_kind), + Error::<T>::InvalidPqMetadata + ); + ensure!( + metadata.key_strength_bits > 0, + Error::<T>::InvalidPqMetadata + ); + ensure!( + metadata.public_key_commitment != H256::zero(), + Error::<T>::InvalidPqMetadata + ); + if let Some(metadata_hash) = metadata.metadata_hash { + ensure!(metadata_hash != H256::zero(), Error::<T>::InvalidPqMetadata); + } + + if let Some(level) = metadata.claimed_nist_level { + ensure!((1..=5).contains(&level), Error::<T>::InvalidPqMetadata); + } + + if let (Some(issued_at), Some(expires_at)) = (metadata.issued_at, metadata.expires_at) { + ensure!(issued_at <= expires_at, Error::<T>::InvalidPqMetadata); + } + + Ok(()) } -/// Apply slashing -pub fn apply_slashing<T: Config>( - _validator: T::AccountId, - _reason: SlashingReason, +/// Validate an opaque PQ proof envelope against claimed PQ metadata. +/// +/// This only checks internal consistency and expiry windows so off-chain clients can +/// decide whether to perform full cryptographic verification. +pub fn validate_pq_proof_envelope<T: Config>( + metadata: &PqReadinessMetadata<BlockNumberFor<T>>, + proof: &DefaultPqProof<BlockNumberFor<T>>, ) -> DispatchResult { - // This function should be implemented in the pallet where storage access is available - // For now, just return Ok - Ok(()) + validate_pq_readiness_metadata::<T>(metadata)?; + + ensure!(!proof.proof.is_empty(), Error::<T>::InvalidPqProof); + ensure!( + proof.proof.iter().any(|byte| *byte != 0), + Error::<T>::InvalidPqProof + ); + ensure!( + proof.statement_hash != H256::zero(), + Error::<T>::InvalidPqProof + ); + ensure!( + proof.public_key_commitment != H256::zero(), + Error::<T>::InvalidPqProof + ); + ensure!( + is_structurally_known_pq_algorithm(&proof.algorithm), + Error::<T>::InvalidPqProof + ); + ensure!( + is_structurally_known_pq_proof_kind(&proof.proof_kind), + Error::<T>::InvalidPqProof + ); + ensure!( + proof.algorithm == metadata.algorithm, + Error::<T>::PqMetadataMismatch + ); + ensure!( + proof.proof_kind == metadata.proof_kind, + Error::<T>::PqMetadataMismatch + ); + ensure!( + proof.public_key_commitment == metadata.public_key_commitment, + Error::<T>::PqMetadataMismatch + ); + if let Some(auxiliary_hash) = proof.auxiliary_hash { + ensure!(auxiliary_hash != H256::zero(), Error::<T>::InvalidPqProof); + } + + if matches!( + proof.proof_kind, + PqProofKind::Attestation | PqProofKind::Transcript + ) { + ensure!(!proof.context.is_empty(), Error::<T>::InvalidPqProof); + } + + if let Some(issued_at) = metadata.issued_at { + ensure!(proof.submitted_at >= issued_at, Error::<T>::InvalidPqProof); + } + + if let Some(expires_at) = metadata.expires_at { + ensure!( + proof.submitted_at <= expires_at, + Error::<T>::PqMetadataExpired + ); + } + + Ok(()) } -/// Verify PQC Dilithium signature -pub fn verify_pqc_signature( - message: &[u8], - signature_bytes: &[u8], - public_key_bytes: &[u8], -) -> bool { - #[cfg(feature = "std")] - { - use pqcrypto_dilithium::dilithium5::{verify_detached_signature, DetachedSignature, PublicKey}; - use pqcrypto_traits::sign::{DetachedSignature as _, PublicKey as _}; - - if let (Ok(sig), Ok(pk)) = ( - DetachedSignature::from_bytes(signature_bytes), - PublicKey::from_bytes(public_key_bytes), - ) { - return verify_detached_signature(&sig, message, &pk).is_ok(); - } - } - - // For no_std or if parsing fails, we use a placeholder that would fail validation - // In a real production deployment, a no_std compatible PQC library would be used. - false +/// Distribute rewards to the miner and all current stakers. +pub fn distribute_rewards<T: Config>( + miner: T::AccountId, + stakers: Vec<ValidatorStake<T::AccountId, BalanceOf<T>>>, + reward: BlockReward<BalanceOf<T>>, +) -> DispatchResult { + let total_stake: BalanceOf<T> = stakers + .iter() + .fold(Zero::zero(), |acc, stake| acc.saturating_add(stake.stake)); + + if total_stake.is_zero() { + // No stakers: the miner receives the entire block reward, so none of the reward + // is silently dropped (the 60% staker share would otherwise be un-minted). + let _ = pallet_balances::Pallet::<T>::deposit_creating(&miner, reward.total); + return Ok(()); + } + + let _ = pallet_balances::Pallet::<T>::deposit_creating(&miner, reward.miner_reward); + + // Proportional distribution with saturating math. The final staker absorbs any + // integer-division dust so the entire stakers_reward is always paid out. + let mut distributed: BalanceOf<T> = Zero::zero(); + let last_index = stakers.len().saturating_sub(1); + for (index, staker) in stakers.iter().enumerate() { + let staker_reward = if index == last_index { + reward.stakers_reward.saturating_sub(distributed) + } else { + reward.stakers_reward.saturating_mul(staker.stake) / total_stake + }; + distributed = distributed.saturating_add(staker_reward); + let _ = pallet_balances::Pallet::<T>::deposit_creating(&staker.account, staker_reward); + } + + Ok(()) } diff --git a/pallets/pallet-ghost-consensus/src/lib.rs b/pallets/pallet-ghost-consensus/src/lib.rs index a6ddec9..5fe7f5b 100644 --- a/pallets/pallet-ghost-consensus/src/lib.rs +++ b/pallets/pallet-ghost-consensus/src/lib.rs @@ -1,573 +1,1048 @@ -//! # Ghost Consensus Pallet +//! Ghost consensus pallet — the Proof-of-Stake economic layer of the hybrid chain. //! -//! This pallet implements a hybrid Proof-of-Work (PoW) and Proof-of-Stake (PoS) consensus mechanism -//! for the Ghost blockchain. It combines the security of PoW with the efficiency of PoS. +//! Real block production is Proof-of-Work, authored by the node via `sc-consensus-pow` +//! (Aura and GRANDPA have been removed from the node and runtime). This pallet is the +//! on-chain economic + validation layer on top of that PoW: +//! 1. A miner submits the PoW block header it solved. +//! 2. A stake-weighted validator finalizes that header (optionally attesting with a real +//! ML-DSA-87 / FIPS 204 post-quantum signature that is verified on-chain) and rewards +//! are paid out (40% miner / 60% stakers); misbehavior is slashed and the funds burned. //! -//! ## Overview -//! -//! The Ghost consensus mechanism works as follows: -//! 1. Miners perform PoW to find a valid nonce -//! 2. Validators are selected based on their stake weight -//! 3. Selected validators sign the PoW-mined blocks -//! 4. Rewards are distributed: 40% to miner, 60% to stakers -//! 5. Slashing for misbehavior (double-signing, invalid blocks, downtime) +//! When the active staker set is empty there is no validator to select, so a submitted +//! block is finalized immediately with the full reward paid to the miner. The pallet also +//! exposes the current mining difficulty to the node through `DifficultyApi`. #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + +use codec::Encode; pub use pallet::*; -pub mod types; pub mod functions; +pub mod pq_verify; +pub mod types; +pub mod weights; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; #[cfg(test)] mod mock; #[cfg(test)] mod tests; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; -use frame_support::{pallet_prelude::*, traits::Currency}; +use alloc::vec::Vec; +use frame_support::{ + pallet_prelude::*, + traits::{Currency, ExistenceRequirement, WithdrawReasons}, +}; use frame_system::pallet_prelude::*; -use sp_runtime::traits::{BlakeTwo256, Hash, SaturatedConversion, Saturating, Zero}; - -use crate::types::*; -use crate::functions::*; - -type BalanceOf<T> = <T as pallet_balances::Config>::Balance; +use sp_runtime::traits::{ + AccountIdConversion, BlakeTwo256, Hash, SaturatedConversion, Saturating, Zero, +}; + +use crate::functions::{ + calculate_block_reward, calculate_difficulty_adjustment, distribute_rewards, + select_pos_validator, validate_block_header, validate_misbehavior_evidence, + validate_pq_proof_envelope, validate_pq_readiness_metadata, +}; +use crate::types::{ + ConsensusPhase, DefaultPqProof, GenesisHeaderInit, GhostBlockHeader, MisbehaviorEvidence, + PqAlgorithm, PqProofKind, PqReadinessMetadata, SlashingReason, ValidatorStake, +}; + +pub type BalanceOf<T> = <T as pallet_balances::Config>::Balance; + +/// FIPS 204 context string binding validator attestation signatures to this chain +/// and use-case (domain separation), per ML-DSA's context-string parameter. +pub const GHOST_VALIDATOR_CTX: &[u8] = b"ghost-validator-v1"; + +/// Number of recent Ghost blocks whose header/miner/validator records are retained. +/// Older entries are pruned on finalization to bound on-chain state growth. +pub const HEADER_RETENTION: u32 = 1024; #[frame_support::pallet] pub mod pallet { - use super::*; - - /// The pallet's configuration trait. - #[pallet::config] - pub trait Config: frame_system::Config + pallet_balances::Config { - /// The overarching runtime event type. - type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; - - /// Weight information for extrinsics. - type WeightInfo: WeightInfo; - - /// Block reward amount - #[pallet::constant] - type BlockReward: Get<BalanceOf<Self>>; - - /// Minimum stake required to participate in PoS - #[pallet::constant] - type MinStake: Get<BalanceOf<Self>>; - - /// Maximum downtime blocks before slashing - #[pallet::constant] - type MaxDowntimeBlocks: Get<u32>; - - /// Double sign slash percentage - #[pallet::constant] - type DoubleSignSlashPercentage: Get<u8>; - - /// Invalid block slash percentage - #[pallet::constant] - type InvalidBlockSlashPercentage: Get<u8>; - - /// Downtime slash percentage - #[pallet::constant] - type DowntimeSlashPercentage: Get<u8>; - - /// The pallet ID - #[pallet::constant] - type PalletId: Get<frame_support::PalletId>; - } - - #[pallet::pallet] - pub struct Pallet<T>(_); - - /// Current mining difficulty - #[pallet::storage] - pub type Difficulty<T: Config> = StorageValue<_, u64, ValueQuery>; - - /// Current consensus phase - #[pallet::storage] - pub type CurrentPhase<T: Config> = StorageValue<_, ConsensusPhase, ValueQuery>; - - /// Block headers storage - #[pallet::storage] - pub type BlockHeaders<T: Config> = StorageMap<_, Blake2_128Concat, u32, GhostBlockHeader>; - - /// Validator stakes - #[pallet::storage] - pub type ValidatorStakes<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>>; - - /// Last active block for validators - #[pallet::storage] - pub type LastActiveBlock<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>; - - /// Double sign reports - #[pallet::storage] - pub type DoubleSignReports<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, bool, ValueQuery>; - - /// Invalid block reports - #[pallet::storage] - pub type InvalidBlockReports<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, bool, ValueQuery>; - - /// Slashing records - #[pallet::storage] - #[pallet::unbounded] - pub type SlashingRecords<T: Config> = StorageValue<_, Vec<(T::AccountId, SlashingReason, BalanceOf<T>, BlockNumberFor<T>)>, ValueQuery>; - - /// Events that functions in this pallet can emit. - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event<T: Config> { - /// A new block has been mined - BlockMined { - block_number: u32, - miner: T::AccountId, - nonce: u64, - }, - /// A validator has been selected for PoS - ValidatorSelected { - validator: T::AccountId, - weight: u64, - }, - /// Block rewards have been distributed - RewardsDistributed { - miner: T::AccountId, - miner_reward: BalanceOf<T>, - stakers_reward: BalanceOf<T>, - }, - /// A validator has been slashed - ValidatorSlashed { - validator: T::AccountId, - reason: SlashingReason, - amount: BalanceOf<T>, - }, - /// Difficulty has been adjusted - DifficultyAdjusted { - old_difficulty: u64, - new_difficulty: u64, - }, - } - - /// Errors that can be returned by this pallet. - #[pallet::error] - pub enum Error<T> { - /// Invalid block number - InvalidBlockNumber, - /// Invalid parent hash - InvalidParentHash, - /// Invalid PoW - InvalidPow, - /// Difficulty too low - DifficultyTooLow, - /// Difficulty too high - DifficultyTooHigh, - /// Insufficient stake - InsufficientStake, - /// Not a validator - NotAValidator, - /// Block not found - BlockNotFound, - /// Invalid phase transition - InvalidPhaseTransition, - /// Invalid PQC signature - InvalidPqcSignature, - /// PQC public key not found - PqcPublicKeyNotFound, - } - - /// PQC Public Keys for validators - #[pallet::storage] - pub type ValidatorPqcPublicKeys<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, [u8; 2592]>; - - /// Rolling window of the last 100 block producers - #[pallet::storage] - pub type RecentBlockProducers<T: Config> = StorageValue<_, BoundedVec<T::AccountId, ConstU32<100>>, ValueQuery>; - - /// Current entropy value (scaled by 10^6) - #[pallet::storage] - pub type CurrentEntropy<T> = StorageValue<_, u64, ValueQuery>; - - /// The pallet's dispatchable functions. - #[pallet::call] - impl<T: Config> Pallet<T> { - /// Submit a mined block with PoW solution - #[pallet::call_index(0)] - #[pallet::weight(<T as Config>::WeightInfo::submit_block())] - pub fn submit_block( - origin: OriginFor<T>, - block_header: GhostBlockHeader, - ) -> DispatchResult { - let miner = ensure_signed(origin)?; - - // Validate the block header - let parent_header = BlockHeaders::<T>::get(block_header.number - 1) - .ok_or(Error::<T>::BlockNotFound)?; - - validate_block_header::<T>(&block_header, &parent_header)?; - - // Store the block header - BlockHeaders::<T>::insert(block_header.number, block_header.clone()); - - // Update recent block producers - let mut recent = RecentBlockProducers::<T>::get(); - if recent.len() >= 100 { - recent.remove(0); - } - let _ = recent.try_push(miner.clone()); - RecentBlockProducers::<T>::put(recent); - - // Update last active block for miner - LastActiveBlock::<T>::insert(&miner, block_header.number); - - // Transition to PoS validation phase - CurrentPhase::<T>::put(ConsensusPhase::PosValidation); - - Self::deposit_event(Event::BlockMined { - block_number: block_header.number, - miner: miner.clone(), - nonce: block_header.nonce, - }); - - Ok(()) - } - - /// Stake tokens to participate in PoS validation - #[pallet::call_index(1)] - #[pallet::weight(<T as Config>::WeightInfo::stake())] - pub fn stake( - origin: OriginFor<T>, - amount: BalanceOf<T>, - ) -> DispatchResult { - let staker = ensure_signed(origin)?; - - ensure!(amount >= T::MinStake::get(), Error::<T>::InsufficientStake); - - - // Update stake - let current_stake = ValidatorStakes::<T>::get(&staker).unwrap_or_default(); - ValidatorStakes::<T>::insert(&staker, current_stake + amount); - - Ok(()) - } - - /// Unstake tokens - #[pallet::call_index(2)] - #[pallet::weight(<T as Config>::WeightInfo::unstake())] - pub fn unstake( - origin: OriginFor<T>, - amount: BalanceOf<T>, - ) -> DispatchResult { - let staker = ensure_signed(origin)?; - - let current_stake = ValidatorStakes::<T>::get(&staker) - .ok_or(Error::<T>::NotAValidator)?; - - ensure!(current_stake >= amount, Error::<T>::InsufficientStake); - - // Update stake - ValidatorStakes::<T>::insert(&staker, current_stake - amount); - - - Ok(()) - } - - /// Select and validate PoS validator - #[pallet::call_index(3)] - #[pallet::weight(<T as Config>::WeightInfo::validate_block())] - pub fn validate_block( - origin: OriginFor<T>, - block_number: u32, - pqc_signature: PqcSignature, - ) -> DispatchResult { - let validator = ensure_signed(origin)?; - - // Check if validator has stake - ensure!(ValidatorStakes::<T>::contains_key(&validator), Error::<T>::NotAValidator); - - let block_header = BlockHeaders::<T>::get(block_number) - .ok_or(Error::<T>::BlockNotFound)?; - - // Ensure we are in PoS validation phase - ensure!(CurrentPhase::<T>::get() == ConsensusPhase::PosValidation, Error::<T>::InvalidPhaseTransition); - - // Select validator based on stake - let stakers = ValidatorStakes::<T>::iter() - .map(|(account, stake)| ValidatorStake { - account, - stake, - weight: stake.saturated_into(), - }) - .collect::<Vec<_>>(); - - let seed = BlakeTwo256::hash_of(&(block_number, frame_system::Pallet::<T>::block_number())); - let selection = select_pos_validator::<T>(stakers, seed) - .ok_or(Error::<T>::NotAValidator)?; - - ensure!(selection.validator == validator, Error::<T>::NotAValidator); - - // Verify PQC Signature - let pqc_public_key = ValidatorPqcPublicKeys::<T>::get(&validator) - .ok_or(Error::<T>::PqcPublicKeyNotFound)?; - - let message = BlakeTwo256::hash_of(&(validator.clone(), block_number)); - ensure!( - verify_pqc_signature(message.as_bytes(), &pqc_signature.0, &pqc_public_key), - Error::<T>::InvalidPqcSignature - ); - - // Sign the block - let signature = BlakeTwo256::hash_of(&(validator.clone(), block_number)); - let mut signed_header = block_header; - signed_header.validator_signature = Some(signature); - signed_header.pqc_signature = Some(pqc_signature); - - BlockHeaders::<T>::insert(block_number, signed_header); - - // Distribute rewards - let reward = calculate_block_reward::<T>(T::BlockReward::get()); - let miner = Self::get_miner_for_block(block_number)?; - let stakers_list = ValidatorStakes::<T>::iter() - .map(|(account, stake)| ValidatorStake { - account, - stake, - weight: stake.saturated_into(), - }) - .collect(); - - distribute_rewards::<T>(miner.clone(), stakers_list, reward.clone())?; - - // Update last active block - LastActiveBlock::<T>::insert(&validator, block_number); - - // Transition to finalization phase - CurrentPhase::<T>::put(ConsensusPhase::Finalization); - - Self::deposit_event(Event::ValidatorSelected { - validator, - weight: selection.weight, - }); - - Self::deposit_event(Event::RewardsDistributed { - miner, - miner_reward: reward.miner_reward, - stakers_reward: reward.stakers_reward, - }); - - Ok(()) - } - - /// Register PQC public key - #[pallet::call_index(5)] - #[pallet::weight(<T as Config>::WeightInfo::register_pqc_key())] - pub fn register_pqc_key( - origin: OriginFor<T>, - public_key: [u8; 2592], - ) -> DispatchResult { - let validator = ensure_signed(origin)?; - ValidatorPqcPublicKeys::<T>::insert(&validator, public_key); - Ok(()) - } - - /// Report validator misbehavior - #[pallet::call_index(4)] - #[pallet::weight(<T as Config>::WeightInfo::report_misbehavior())] - pub fn report_misbehavior( - origin: OriginFor<T>, - validator: T::AccountId, - reason: SlashingReason, - ) -> DispatchResult { - let _reporter = ensure_signed(origin)?; - - match reason { - SlashingReason::DoubleSigning => { - DoubleSignReports::<T>::insert(&validator, true); - }, - SlashingReason::InvalidBlock => { - InvalidBlockReports::<T>::insert(&validator, true); - }, - _ => {}, - } - - // Apply slashing - simplified for now - let slash_percentage = match reason { - SlashingReason::DoubleSigning => T::DoubleSignSlashPercentage::get(), - SlashingReason::InvalidBlock => T::InvalidBlockSlashPercentage::get(), - SlashingReason::Downtime => T::DowntimeSlashPercentage::get(), - SlashingReason::Other => 10, - }; - - let current_stake = ValidatorStakes::<T>::get(&validator).unwrap_or_default(); - let slash_amount = (current_stake * slash_percentage.into()) / 100u32.into(); - ValidatorStakes::<T>::insert(&validator, current_stake.saturating_sub(slash_amount)); - - Self::deposit_event(Event::ValidatorSlashed { - validator, - reason, - amount: slash_amount, - }); - - Ok(()) - } - } - - /// Helper functions - impl<T: Config> Pallet<T> { - - /// Get miner for a block - fn get_miner_for_block(_block_number: u32) -> Result<T::AccountId, Error<T>> { - // This would need to be implemented based on how miners are tracked - // For now, return a placeholder - Err(Error::<T>::BlockNotFound) - } - - /// Distribute block rewards to miner and stakers - pub fn distribute_block_rewards( - miner: T::AccountId, - _block_number: u32, - ) -> DispatchResult { - let total_reward = T::BlockReward::get(); - let reward = calculate_block_reward::<T>(total_reward); - - // Reward the miner (40%) - let _ = pallet_balances::Pallet::<T>::deposit_creating(&miner, reward.miner_reward); - - // Collect all stakers - let stakers: Vec<ValidatorStake<T::AccountId, BalanceOf<T>>> = ValidatorStakes::<T>::iter() - .map(|(account, stake)| ValidatorStake { - account, - stake, - weight: stake.saturated_into(), - }) - .collect(); - - // Distribute to stakers proportionally (60%) - if !stakers.is_empty() { - let total_stake: BalanceOf<T> = stakers.iter().fold(Zero::zero(), |acc, s| acc + s.stake); - if !total_stake.is_zero() { - for staker in stakers { - let staker_reward = (reward.stakers_reward * staker.stake) / total_stake; - let _ = pallet_balances::Pallet::<T>::deposit_creating(&staker.account, staker_reward); - } - } - } - - Self::deposit_event(Event::RewardsDistributed { - miner, - miner_reward: reward.miner_reward, - stakers_reward: reward.stakers_reward, - }); - - Ok(()) - } - - /// Check and apply slashing for downtime - pub fn check_downtime_slashing() { - let current_block = frame_system::Pallet::<T>::block_number().saturated_into::<u32>(); - let max_downtime = T::MaxDowntimeBlocks::get(); - - for (validator, last_active) in LastActiveBlock::<T>::iter() { - if current_block.saturating_sub(last_active) > max_downtime { - // Apply downtime slashing - let slash_percentage = T::DowntimeSlashPercentage::get(); - if let Some(stake) = ValidatorStakes::<T>::get(&validator) { - let slash_amount = (stake * slash_percentage.into()) / 100u32.into(); - let new_stake = stake.saturating_sub(slash_amount); - ValidatorStakes::<T>::insert(&validator, new_stake); - - // Record slashing - let mut records = SlashingRecords::<T>::get(); - records.push(( - validator.clone(), - SlashingReason::Downtime, - slash_amount, - frame_system::Pallet::<T>::block_number(), - )); - SlashingRecords::<T>::put(records); - - Self::deposit_event(Event::ValidatorSlashed { - validator, - reason: SlashingReason::Downtime, - amount: slash_amount, - }); - } - } - } - } - - /// Adjust difficulty based on block time and entropy - pub fn adjust_difficulty() { - let current_difficulty = Difficulty::<T>::get(); - let target_block_time = 5u64; // 5 seconds - - // In a real implementation, calculate actual block time from recent blocks - // For now, use a simple adjustment - let actual_block_time = 5u64; // Placeholder - - // Calculate entropy - let producers = RecentBlockProducers::<T>::get().to_vec(); - let entropy = calculate_entropy::<T>(producers); - CurrentEntropy::<T>::put(entropy); - - let new_difficulty = calculate_difficulty_adjustment::<T>( - current_difficulty, - actual_block_time, - target_block_time, - entropy, - ); - - if new_difficulty != current_difficulty { - Difficulty::<T>::put(new_difficulty); - Self::deposit_event(Event::DifficultyAdjusted { - old_difficulty: current_difficulty, - new_difficulty, - }); - } - } - } - - /// Hooks for automatic behavior - #[pallet::hooks] - impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { - /// Called at the beginning of each block - fn on_initialize(n: BlockNumberFor<T>) -> Weight { - // Check for downtime slashing every 10 blocks - if (n % 10u32.into()).is_zero() { - Self::check_downtime_slashing(); - } - - // Adjust difficulty every 100 blocks - if (n % 100u32.into()).is_zero() { - Self::adjust_difficulty(); - } - - Weight::from_parts(10_000, 0) - } - - /// Called at the end of each block - fn on_finalize(_n: BlockNumberFor<T>) { - // Transition back to PoW mining phase for next block - if CurrentPhase::<T>::get() == ConsensusPhase::Finalization { - CurrentPhase::<T>::put(ConsensusPhase::PowMining); - } - } - } + use super::*; + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_balances::Config + pallet_timestamp::Config + { + type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; + type WeightInfo: WeightInfo; + + /// Target average block time in milliseconds. PoW difficulty retargets toward this. + #[pallet::constant] + type TargetBlockTime: Get<u64>; + /// How often (in blocks) PoW difficulty is retargeted. + #[pallet::constant] + type DifficultyAdjustmentPeriod: Get<u32>; + + #[pallet::constant] + type BlockReward: Get<BalanceOf<Self>>; + #[pallet::constant] + type MinStake: Get<BalanceOf<Self>>; + #[pallet::constant] + type MaxDowntimeBlocks: Get<u32>; + #[pallet::constant] + type MaxValidationBlocks: Get<u32>; + #[pallet::constant] + type MaxValidators: Get<u32>; + #[pallet::constant] + type MaxSlashingRecords: Get<u32>; + #[pallet::constant] + type DoubleSignSlashPercentage: Get<u8>; + #[pallet::constant] + type InvalidBlockSlashPercentage: Get<u8>; + #[pallet::constant] + type DowntimeSlashPercentage: Get<u8>; + #[pallet::constant] + type PalletId: Get<frame_support::PalletId>; + } + + #[pallet::pallet] + pub struct Pallet<T>(_); + + #[pallet::type_value] + pub fn DefaultDifficulty<T: Config>() -> u64 { + // Conventional difficulty (higher = harder). A modest devnet default a CPU can mine + // quickly at genesis; on-chain retargeting then tunes it toward the target block time. + 100_000 + } + + #[pallet::type_value] + pub fn DefaultPhase<T: Config>() -> ConsensusPhase { + ConsensusPhase::PowMining + } + + #[pallet::storage] + pub type Difficulty<T: Config> = StorageValue<_, u64, ValueQuery, DefaultDifficulty<T>>; + + /// Block number recorded at the last difficulty retarget. + #[pallet::storage] + pub type LastRetargetBlock<T: Config> = StorageValue<_, u32, ValueQuery>; + + /// Timestamp (ms) recorded at the last difficulty retarget. + #[pallet::storage] + pub type LastRetargetMoment<T: Config> = StorageValue<_, u64, ValueQuery>; + + #[pallet::storage] + pub type CurrentPhase<T: Config> = StorageValue<_, ConsensusPhase, ValueQuery, DefaultPhase<T>>; + + #[pallet::storage] + pub type BlockHeaders<T: Config> = StorageMap<_, Blake2_128Concat, u32, GhostBlockHeader>; + + #[pallet::storage] + pub type BlockMiners<T: Config> = StorageMap<_, Blake2_128Concat, u32, T::AccountId>; + + #[pallet::storage] + pub type PendingValidationBlock<T: Config> = StorageValue<_, u32>; + + #[pallet::storage] + pub type PhaseStartedAt<T: Config> = StorageValue<_, u32, ValueQuery>; + + #[pallet::storage] + pub type ValidatorStakes<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>>; + + #[pallet::storage] + pub type ValidatorCount<T: Config> = StorageValue<_, u32, ValueQuery>; + + #[pallet::storage] + pub type LastActiveBlock<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>; + + #[pallet::storage] + pub type DoubleSignReports<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, bool, ValueQuery>; + + #[pallet::storage] + pub type InvalidBlockReports<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, bool, ValueQuery>; + + #[pallet::storage] + pub type SlashingRecords<T: Config> = StorageValue< + _, + BoundedVec< + ( + T::AccountId, + SlashingReason, + BalanceOf<T>, + BlockNumberFor<T>, + ), + T::MaxSlashingRecords, + >, + ValueQuery, + >; + + #[pallet::storage] + pub type PqReadinessRegistry<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, PqReadinessMetadata<BlockNumberFor<T>>>; + + #[pallet::storage] + pub type PqReadinessAttestations<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, DefaultPqProof<BlockNumberFor<T>>>; + + /// The validator selected (at submit time) to validate the pending block. + /// Storing the selection removes the race where the selected validator could + /// change between `validate_block` attempts. + #[pallet::storage] + pub type PendingValidator<T: Config> = StorageValue<_, T::AccountId>; + + /// Which validator validated each block. Used for slashing attribution. + #[pallet::storage] + pub type BlockValidators<T: Config> = StorageMap<_, Blake2_128Concat, u32, T::AccountId>; + + /// Registered ML-DSA (FIPS 204) public key per validator. The bound (2592 bytes) + /// fits the largest parameter set, ML-DSA-87 ("Dilithium-5"). + #[pallet::storage] + pub type ValidatorMlDsaKey<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, BoundedVec<u8, ConstU32<2592>>>; + + /// The ML-DSA parameter set a validator registered their key under. + #[pallet::storage] + pub type ValidatorMlDsaAlgo<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, PqAlgorithm>; + + /// Verified post-quantum attestations: (attester, statement hash) -> block recorded. + #[pallet::storage] + pub type PqVerifiedAttestations<T: Config> = + StorageMap<_, Blake2_128Concat, (T::AccountId, sp_core::H256), BlockNumberFor<T>>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + BlockMined { + block_number: u32, + miner: T::AccountId, + nonce: u64, + }, + ValidatorSelected { + validator: T::AccountId, + weight: u64, + }, + RewardsDistributed { + miner: T::AccountId, + miner_reward: BalanceOf<T>, + stakers_reward: BalanceOf<T>, + }, + ValidatorSlashed { + validator: T::AccountId, + reason: SlashingReason, + amount: BalanceOf<T>, + }, + DifficultyAdjusted { + old_difficulty: u64, + new_difficulty: u64, + }, + ValidationTimedOut { + block_number: u32, + }, + PqReadinessRegistered { + account: T::AccountId, + algorithm: PqAlgorithm, + proof_kind: PqProofKind, + }, + PqReadinessAttested { + account: T::AccountId, + algorithm: PqAlgorithm, + proof_kind: PqProofKind, + statement_hash: sp_core::H256, + }, + PqReadinessRemoved { + account: T::AccountId, + }, + Staked { + staker: T::AccountId, + amount: BalanceOf<T>, + total_stake: BalanceOf<T>, + }, + Unstaked { + staker: T::AccountId, + amount: BalanceOf<T>, + remaining: BalanceOf<T>, + }, + ValidatorMlDsaKeyRegistered { + account: T::AccountId, + algorithm: PqAlgorithm, + }, + PqSignatureVerified { + attester: T::AccountId, + algorithm: PqAlgorithm, + statement_hash: sp_core::H256, + }, + } + + #[pallet::error] + pub enum Error<T> { + InvalidBlockNumber, + InvalidParentHash, + InvalidPow, + DifficultyTooLow, + DifficultyTooHigh, + DifficultyMismatch, + InsufficientStake, + NotAValidator, + BlockNotFound, + InvalidPhaseTransition, + InvalidEvidence, + TooManyValidators, + TooManySlashingRecords, + PhaseTimeoutNotReached, + PqReadinessNotFound, + InvalidPqMetadata, + InvalidPqProof, + PqMetadataMismatch, + PqMetadataExpired, + MlDsaKeyInvalid, + MlDsaSignatureInvalid, + MlDsaSignatureMissing, + MlDsaNotRegistered, + NotSelectedValidator, + AttestationAlreadyRecorded, + } + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig<T: Config> { + pub genesis_header: Option<GenesisHeaderInit>, + pub validator_stakes: Vec<(T::AccountId, BalanceOf<T>)>, + } + + #[pallet::genesis_build] + impl<T: Config> BuildGenesisConfig for GenesisConfig<T> { + fn build(&self) { + if let Some(header) = &self.genesis_header { + let header = GhostBlockHeader { + number: header.0, + parent_hash: header.1, + state_root: header.2, + extrinsics_root: header.3, + nonce: header.4, + difficulty: header.5, + validator_signature: header.6, + }; + BlockHeaders::<T>::insert(header.number, header); + } + + let mut count = 0u32; + for (account, stake) in self + .validator_stakes + .iter() + .take(T::MaxValidators::get() as usize) + { + if *stake >= T::MinStake::get() { + ValidatorStakes::<T>::insert(account, stake); + LastActiveBlock::<T>::insert(account, 0u32); + // Back the genesis stake with real tokens in the pallet account so + // that unstaking and slashing have funds to move/burn. + let _ = pallet_balances::Pallet::<T>::deposit_creating( + &Pallet::<T>::account_id(), + *stake, + ); + count = count.saturating_add(1); + } + } + ValidatorCount::<T>::put(count); + } + } + + #[pallet::call] + impl<T: Config> Pallet<T> { + #[pallet::call_index(0)] + #[pallet::weight(<T as Config>::WeightInfo::submit_block())] + pub fn submit_block( + origin: OriginFor<T>, + block_header: GhostBlockHeader, + ) -> DispatchResult { + let miner = ensure_signed(origin)?; + ensure!( + CurrentPhase::<T>::get() == ConsensusPhase::PowMining, + Error::<T>::InvalidPhaseTransition + ); + + let parent_header = block_header + .number + .checked_sub(1) + .and_then(BlockHeaders::<T>::get) + .ok_or(Error::<T>::BlockNotFound)?; + + validate_block_header::<T>(&block_header, &parent_header)?; + + BlockMiners::<T>::insert(block_header.number, miner.clone()); + BlockHeaders::<T>::insert(block_header.number, block_header.clone()); + LastActiveBlock::<T>::insert(&miner, block_header.number); + + Self::deposit_event(Event::BlockMined { + block_number: block_header.number, + miner: miner.clone(), + nonce: block_header.nonce, + }); + + // Select the PoS validator now (at submit time) using the parent block + // hash as entropy. Storing the selection binds it to a value the submitter + // cannot grind and removes the per-call re-selection race in validate_block. + let stakers = Self::all_stakers(); + let seed = BlakeTwo256::hash_of(&( + block_header.number, + frame_system::Pallet::<T>::parent_hash(), + )); + match select_pos_validator::<T>(stakers, seed) { + Some(selection) => { + // Stakers exist: enter PoS validation and await the selected validator. + PendingValidationBlock::<T>::put(block_header.number); + PhaseStartedAt::<T>::put( + frame_system::Pallet::<T>::block_number().saturated_into::<u32>(), + ); + PendingValidator::<T>::put(selection.validator); + CurrentPhase::<T>::put(ConsensusPhase::PosValidation); + } + None => { + // No stakers means there is no validator to select and no PoS + // validation to perform. Finalize the PoW block immediately and pay + // the miner the full reward instead of entering PosValidation and + // stalling until the validation timeout fires; the chain stays in + // PowMining, ready for the next block. + Self::finalize_without_validator(block_header.number, &miner)?; + } + } + + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight(<T as Config>::WeightInfo::stake())] + pub fn stake(origin: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult { + let staker = ensure_signed(origin)?; + ensure!(amount >= T::MinStake::get(), Error::<T>::InsufficientStake); + let is_new_validator = !ValidatorStakes::<T>::contains_key(&staker); + if is_new_validator { + ensure!( + ValidatorCount::<T>::get() < T::MaxValidators::get(), + Error::<T>::TooManyValidators + ); + } + + <pallet_balances::Pallet<T> as Currency<T::AccountId>>::transfer( + &staker, + &Self::account_id(), + amount, + ExistenceRequirement::AllowDeath, + )?; + + let current_stake = ValidatorStakes::<T>::get(&staker).unwrap_or_default(); + let new_total = current_stake.saturating_add(amount); + ValidatorStakes::<T>::insert(&staker, new_total); + if is_new_validator { + ValidatorCount::<T>::mutate(|count| *count = count.saturating_add(1)); + } + + Self::deposit_event(Event::Staked { + staker, + amount, + total_stake: new_total, + }); + Ok(()) + } + + #[pallet::call_index(2)] + #[pallet::weight(<T as Config>::WeightInfo::unstake())] + pub fn unstake(origin: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult { + let staker = ensure_signed(origin)?; + let current_stake = + ValidatorStakes::<T>::get(&staker).ok_or(Error::<T>::NotAValidator)?; + ensure!(current_stake >= amount, Error::<T>::InsufficientStake); + + let new_stake = current_stake - amount; + // A partial unstake must not leave a sub-minimum stake registered (which would + // keep an under-collateralized validator active). Check before any state change. + if !new_stake.is_zero() { + ensure!( + new_stake >= T::MinStake::get(), + Error::<T>::InsufficientStake + ); + } + + // Return the staked funds FIRST: if the transfer fails we exit before mutating + // the stake records, so records can never be reduced without returning tokens. + <pallet_balances::Pallet<T> as Currency<T::AccountId>>::transfer( + &Self::account_id(), + &staker, + amount, + ExistenceRequirement::AllowDeath, + )?; + + if new_stake.is_zero() { + ValidatorStakes::<T>::remove(&staker); + ValidatorCount::<T>::mutate(|count| *count = count.saturating_sub(1)); + } else { + ValidatorStakes::<T>::insert(&staker, new_stake); + } + + Self::deposit_event(Event::Unstaked { + staker, + amount, + remaining: new_stake, + }); + Ok(()) + } + + #[pallet::call_index(3)] + #[pallet::weight(<T as Config>::WeightInfo::validate_block())] + pub fn validate_block( + origin: OriginFor<T>, + block_number: u32, + pq_signature: Option<BoundedVec<u8, ConstU32<4627>>>, + ) -> DispatchResult { + let validator = ensure_signed(origin)?; + ensure!( + CurrentPhase::<T>::get() == ConsensusPhase::PosValidation, + Error::<T>::InvalidPhaseTransition + ); + ensure!( + ValidatorStakes::<T>::contains_key(&validator), + Error::<T>::NotAValidator + ); + ensure!( + PendingValidationBlock::<T>::get() == Some(block_number), + Error::<T>::InvalidPhaseTransition + ); + // Only the validator selected (and stored) at submit time may validate. + ensure!( + PendingValidator::<T>::get().as_ref() == Some(&validator), + Error::<T>::NotSelectedValidator + ); + + let block_header = + BlockHeaders::<T>::get(block_number).ok_or(Error::<T>::BlockNotFound)?; + + // Real post-quantum attestation. If the validator has registered an + // ML-DSA (FIPS 204) key, they MUST provide a signature over the canonical + // header message, which is verified on-chain. Validators without a + // registered key use the transitional commitment path. + let commitment = if let Some(algorithm) = ValidatorMlDsaAlgo::<T>::get(&validator) { + let public_key = + ValidatorMlDsaKey::<T>::get(&validator).ok_or(Error::<T>::MlDsaNotRegistered)?; + let signature = pq_signature + .as_ref() + .ok_or(Error::<T>::MlDsaSignatureMissing)?; + // Bind the signature to the immutable header fields + the validator id. + // Excludes `validator_signature` (mutated by this call) and ties the + // signature to this specific block so it cannot be replayed elsewhere. + let message = ( + block_header.number, + block_header.parent_hash, + block_header.state_root, + block_header.extrinsics_root, + block_header.nonce, + block_header.difficulty, + validator.clone(), + ) + .encode(); + ensure!( + pq_verify::verify_ml_dsa( + &algorithm, + public_key.as_slice(), + &message, + signature.as_slice(), + GHOST_VALIDATOR_CTX, + ), + Error::<T>::MlDsaSignatureInvalid + ); + // Compact on-chain commitment proving a valid PQ signature was verified. + BlakeTwo256::hash_of(signature) + } else { + BlakeTwo256::hash_of(&("ghost-validator-v1", validator.clone(), block_number)) + }; + + let weight = ValidatorStakes::<T>::get(&validator) + .unwrap_or_default() + .saturated_into::<u64>(); + + let mut signed_header = block_header; + signed_header.validator_signature = Some(commitment); + BlockHeaders::<T>::insert(block_number, signed_header); + BlockValidators::<T>::insert(block_number, validator.clone()); + + let reward = calculate_block_reward::<T>(T::BlockReward::get()); + let miner = Self::get_miner_for_block(block_number)?; + let stakers = Self::all_stakers(); + distribute_rewards::<T>(miner.clone(), stakers, reward.clone())?; + + LastActiveBlock::<T>::insert(&validator, block_number); + CurrentPhase::<T>::put(ConsensusPhase::Finalization); + + Self::deposit_event(Event::ValidatorSelected { validator, weight }); + Self::deposit_event(Event::RewardsDistributed { + miner, + miner_reward: reward.miner_reward, + stakers_reward: reward.stakers_reward, + }); + + Ok(()) + } + + #[pallet::call_index(4)] + #[pallet::weight(<T as Config>::WeightInfo::report_misbehavior())] + pub fn report_misbehavior( + origin: OriginFor<T>, + validator: T::AccountId, + reason: SlashingReason, + evidence: MisbehaviorEvidence, + ) -> DispatchResult { + let _reporter = ensure_signed(origin)?; + let current_stake = + ValidatorStakes::<T>::get(&validator).ok_or(Error::<T>::NotAValidator)?; + validate_misbehavior_evidence::<T>(&validator, &reason, &evidence)?; + + match reason { + SlashingReason::DoubleSigning => DoubleSignReports::<T>::insert(&validator, true), + SlashingReason::InvalidBlock => InvalidBlockReports::<T>::insert(&validator, true), + SlashingReason::Downtime | SlashingReason::Other => {} + } + + let slash_percentage = match reason { + SlashingReason::DoubleSigning => T::DoubleSignSlashPercentage::get(), + SlashingReason::InvalidBlock => T::InvalidBlockSlashPercentage::get(), + SlashingReason::Downtime => T::DowntimeSlashPercentage::get(), + SlashingReason::Other => 10, + }; + + // Saturating multiply: a naive `current_stake * pct` overflows u128 for + // large stakes (panics in debug, wraps in release). + let slash_amount = current_stake.saturating_mul(slash_percentage.into()) / 100u32.into(); + + // Record the slash first so a full records buffer fails before any state + // change (the dispatch is transactional, but this keeps intent explicit). + let mut records = SlashingRecords::<T>::get(); + records + .try_push(( + validator.clone(), + reason.clone(), + slash_amount, + frame_system::Pallet::<T>::block_number(), + )) + .map_err(|_| Error::<T>::TooManySlashingRecords)?; + SlashingRecords::<T>::put(records); + + Self::reduce_stake_and_burn(&validator, slash_amount); + + Self::deposit_event(Event::ValidatorSlashed { + validator, + reason, + amount: slash_amount, + }); + + Ok(()) + } + + #[pallet::call_index(5)] + #[pallet::weight(<T as Config>::WeightInfo::register_pq_readiness( + metadata.encoded_size().saturated_into::<u32>() + ))] + pub fn register_pq_readiness( + origin: OriginFor<T>, + metadata: PqReadinessMetadata<BlockNumberFor<T>>, + ) -> DispatchResult { + let account = ensure_signed(origin)?; + validate_pq_readiness_metadata::<T>(&metadata)?; + + PqReadinessRegistry::<T>::insert(&account, metadata.clone()); + + Self::deposit_event(Event::PqReadinessRegistered { + account, + algorithm: metadata.algorithm, + proof_kind: metadata.proof_kind, + }); + + Ok(()) + } + + #[pallet::call_index(6)] + #[pallet::weight(<T as Config>::WeightInfo::attest_pq_readiness( + proof.encoded_size().saturated_into::<u32>() + ))] + pub fn attest_pq_readiness( + origin: OriginFor<T>, + proof: DefaultPqProof<BlockNumberFor<T>>, + ) -> DispatchResult { + let account = ensure_signed(origin)?; + let metadata = + PqReadinessRegistry::<T>::get(&account).ok_or(Error::<T>::PqReadinessNotFound)?; + validate_pq_proof_envelope::<T>(&metadata, &proof)?; + + PqReadinessAttestations::<T>::insert(&account, proof.clone()); + + Self::deposit_event(Event::PqReadinessAttested { + account, + algorithm: proof.algorithm, + proof_kind: proof.proof_kind, + statement_hash: proof.statement_hash, + }); + + Ok(()) + } + + #[pallet::call_index(7)] + #[pallet::weight(<T as Config>::WeightInfo::remove_pq_readiness())] + pub fn remove_pq_readiness(origin: OriginFor<T>) -> DispatchResult { + let account = ensure_signed(origin)?; + + PqReadinessRegistry::<T>::remove(&account); + PqReadinessAttestations::<T>::remove(&account); + + Self::deposit_event(Event::PqReadinessRemoved { account }); + + Ok(()) + } + + /// Register an ML-DSA (FIPS 204) public key for the caller. The key bytes are + /// validated for the correct length and a decodable FIPS 204 encoding before + /// being stored. Once registered, the caller's `validate_block` attestations + /// and `verify_pq_signature` calls are checked against this key. ML-DSA-87 is + /// "Dilithium-5" (NIST security level 5). + #[pallet::call_index(8)] + #[pallet::weight(<T as Config>::WeightInfo::register_ml_dsa_key(public_key.len() as u32))] + pub fn register_ml_dsa_key( + origin: OriginFor<T>, + algorithm: PqAlgorithm, + public_key: BoundedVec<u8, ConstU32<2592>>, + ) -> DispatchResult { + let account = ensure_signed(origin)?; + ensure!( + pq_verify::validate_ml_dsa_pk(&algorithm, public_key.as_slice()), + Error::<T>::MlDsaKeyInvalid + ); + ValidatorMlDsaKey::<T>::insert(&account, public_key); + ValidatorMlDsaAlgo::<T>::insert(&account, algorithm.clone()); + + Self::deposit_event(Event::ValidatorMlDsaKeyRegistered { account, algorithm }); + Ok(()) + } + + /// Verify a real ML-DSA (FIPS 204) signature over `message` against the + /// caller's registered public key, and record the verified attestation + /// on-chain. This is genuine post-quantum signature verification executed + /// inside the runtime — not a hash or byte-presence check. + #[pallet::call_index(9)] + #[pallet::weight(<T as Config>::WeightInfo::verify_pq_signature(signature.len() as u32))] + pub fn verify_pq_signature( + origin: OriginFor<T>, + message: BoundedVec<u8, ConstU32<65536>>, + signature: BoundedVec<u8, ConstU32<4627>>, + ctx: BoundedVec<u8, ConstU32<255>>, + ) -> DispatchResult { + let attester = ensure_signed(origin)?; + let public_key = + ValidatorMlDsaKey::<T>::get(&attester).ok_or(Error::<T>::MlDsaNotRegistered)?; + let algorithm = + ValidatorMlDsaAlgo::<T>::get(&attester).ok_or(Error::<T>::MlDsaNotRegistered)?; + + ensure!( + pq_verify::verify_ml_dsa( + &algorithm, + public_key.as_slice(), + message.as_slice(), + signature.as_slice(), + ctx.as_slice(), + ), + Error::<T>::MlDsaSignatureInvalid + ); + + // Replay guard: an attester may record each distinct statement only once. + // Without this, an already-verified attestation could be resubmitted to spam + // events and refresh its recorded block number while conveying no new info. + let statement_hash = BlakeTwo256::hash_of(&message); + ensure!( + !PqVerifiedAttestations::<T>::contains_key((&attester, statement_hash)), + Error::<T>::AttestationAlreadyRecorded + ); + let now = frame_system::Pallet::<T>::block_number(); + PqVerifiedAttestations::<T>::insert((&attester, statement_hash), now); + + Self::deposit_event(Event::PqSignatureVerified { + attester, + algorithm, + statement_hash, + }); + Ok(()) + } + } + + #[pallet::hooks] + impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { + fn on_initialize(n: BlockNumberFor<T>) -> Weight { + if (n % 10u32.into()).is_zero() { + Self::check_downtime_slashing(); + } + + Self::check_validation_timeout(); + + let period: BlockNumberFor<T> = T::DifficultyAdjustmentPeriod::get().max(1).into(); + if (n % period).is_zero() { + Self::adjust_difficulty(); + } + + // Bound on-chain cost: downtime slashing scans up to MaxValidators entries + // and the difficulty retarget touches a few storage items. + T::DbWeight::get().reads_writes( + (T::MaxValidators::get() as u64).saturating_add(4), + (T::MaxValidators::get() as u64).saturating_add(4), + ) + } + + fn on_finalize(_n: BlockNumberFor<T>) { + if CurrentPhase::<T>::get() == ConsensusPhase::Finalization { + if let Some(finalized) = PendingValidationBlock::<T>::get() { + // Prune history beyond the retention window to bound state growth. + if let Some(old) = finalized.checked_sub(HEADER_RETENTION) { + BlockHeaders::<T>::remove(old); + BlockMiners::<T>::remove(old); + BlockValidators::<T>::remove(old); + } + } + PendingValidationBlock::<T>::kill(); + PendingValidator::<T>::kill(); + PhaseStartedAt::<T>::kill(); + CurrentPhase::<T>::put(ConsensusPhase::PowMining); + } + } + } +} + +impl<T: Config> Pallet<T> { + fn account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Reduce a validator's recorded stake by `slash_amount` and burn the + /// corresponding tokens from the pallet's staking account (the tokens were + /// moved into the pallet account when the validator staked). Burning reduces + /// total issuance, so slashed value is destroyed rather than silently locked + /// in the pallet account forever. + fn reduce_stake_and_burn(validator: &T::AccountId, slash_amount: BalanceOf<T>) { + let current = ValidatorStakes::<T>::get(validator).unwrap_or_default(); + let new_stake = current.saturating_sub(slash_amount); + let burned = current.saturating_sub(new_stake); + if new_stake.is_zero() { + ValidatorStakes::<T>::remove(validator); + ValidatorCount::<T>::mutate(|count| *count = count.saturating_sub(1)); + // Drop the activity record so check_downtime_slashing stops scanning a ghost entry. + LastActiveBlock::<T>::remove(validator); + } else { + ValidatorStakes::<T>::insert(validator, new_stake); + } + let _ = <pallet_balances::Pallet<T> as Currency<T::AccountId>>::withdraw( + &Self::account_id(), + burned, + WithdrawReasons::all(), + ExistenceRequirement::AllowDeath, + ); + } + + fn all_stakers() -> Vec<ValidatorStake<T::AccountId, BalanceOf<T>>> { + ValidatorStakes::<T>::iter() + .map(|(account, stake)| ValidatorStake { + account, + stake, + weight: stake.saturated_into(), + }) + .collect() + } + + fn get_miner_for_block(block_number: u32) -> Result<T::AccountId, Error<T>> { + BlockMiners::<T>::get(block_number).ok_or(Error::<T>::BlockNotFound) + } + + /// Finalize a freshly submitted PoW block when there are no stakers to run PoS + /// validation. The miner receives the entire block reward (`distribute_rewards` + /// pays the whole reward to the miner when the staker set is empty), old history is + /// pruned exactly as `on_finalize` does for the validated path, and the chain stays + /// in the PoW mining phase. This stops a block from stalling in PosValidation (and + /// having to be rescued by the validation timeout) whenever the staker set is empty. + fn finalize_without_validator(block_number: u32, miner: &T::AccountId) -> DispatchResult { + let reward = calculate_block_reward::<T>(T::BlockReward::get()); + distribute_rewards::<T>(miner.clone(), Vec::new(), reward.clone())?; + + if let Some(old) = block_number.checked_sub(HEADER_RETENTION) { + BlockHeaders::<T>::remove(old); + BlockMiners::<T>::remove(old); + BlockValidators::<T>::remove(old); + } + + // The whole reward went to the miner; report it accurately (no staker share). + Self::deposit_event(Event::RewardsDistributed { + miner: miner.clone(), + miner_reward: reward.total, + stakers_reward: Zero::zero(), + }); + Ok(()) + } + + pub fn check_downtime_slashing() { + let current_block = frame_system::Pallet::<T>::block_number().saturated_into::<u32>(); + let max_downtime = T::MaxDowntimeBlocks::get(); + + for (validator, last_active) in LastActiveBlock::<T>::iter() { + if current_block.saturating_sub(last_active) > max_downtime { + let stake = match ValidatorStakes::<T>::get(&validator) { + Some(stake) => stake, + None => continue, + }; + + let slash_amount = + stake.saturating_mul(T::DowntimeSlashPercentage::get().into()) / 100u32.into(); + let mut records = SlashingRecords::<T>::get(); + if records + .try_push(( + validator.clone(), + SlashingReason::Downtime, + slash_amount, + frame_system::Pallet::<T>::block_number(), + )) + .is_ok() + { + Self::reduce_stake_and_burn(&validator, slash_amount); + SlashingRecords::<T>::put(records); + Self::deposit_event(Event::ValidatorSlashed { + validator, + reason: SlashingReason::Downtime, + amount: slash_amount, + }); + } + } + } + } + + /// Retarget PoW difficulty toward `TargetBlockTime` using the real elapsed time + /// (from `pallet_timestamp`) and block count since the last retarget. Difficulty + /// is clamped to at most a 4x change per retarget to damp oscillation. + pub fn adjust_difficulty() { + let now = pallet_timestamp::Pallet::<T>::get().saturated_into::<u64>(); + let current_block = frame_system::Pallet::<T>::block_number().saturated_into::<u32>(); + let last_block = LastRetargetBlock::<T>::get(); + let last_moment = LastRetargetMoment::<T>::get(); + + // First observation just establishes the baseline; we cannot measure a rate yet. + if last_block == 0 && last_moment == 0 { + LastRetargetBlock::<T>::put(current_block); + LastRetargetMoment::<T>::put(now); + return; + } + + let blocks_elapsed = current_block.saturating_sub(last_block); + let time_elapsed = now.saturating_sub(last_moment); + if blocks_elapsed == 0 || time_elapsed == 0 { + return; + } + + let actual_block_time = (time_elapsed / blocks_elapsed as u64).max(1); + let target_block_time = T::TargetBlockTime::get().max(1); + let current_difficulty = Difficulty::<T>::get(); + let proposed = + calculate_difficulty_adjustment::<T>(current_difficulty, actual_block_time, target_block_time); + + // Clamp to [current/4, current*4] so a single retarget cannot swing wildly. + let min_difficulty = (current_difficulty / 4).max(1); + let max_difficulty = current_difficulty.saturating_mul(4).max(1); + let new_difficulty = proposed.clamp(min_difficulty, max_difficulty); + + LastRetargetBlock::<T>::put(current_block); + LastRetargetMoment::<T>::put(now); + + if new_difficulty != current_difficulty { + Difficulty::<T>::put(new_difficulty); + Self::deposit_event(Event::DifficultyAdjusted { + old_difficulty: current_difficulty, + new_difficulty, + }); + } + } + + pub fn check_validation_timeout() { + if CurrentPhase::<T>::get() != ConsensusPhase::PosValidation { + return; + } + + let Some(block_number) = PendingValidationBlock::<T>::get() else { + PendingValidator::<T>::kill(); + PhaseStartedAt::<T>::kill(); + CurrentPhase::<T>::put(ConsensusPhase::PowMining); + return; + }; + + let current_block = frame_system::Pallet::<T>::block_number().saturated_into::<u32>(); + if current_block.saturating_sub(PhaseStartedAt::<T>::get()) <= T::MaxValidationBlocks::get() + { + return; + } + + PendingValidationBlock::<T>::kill(); + PendingValidator::<T>::kill(); + PhaseStartedAt::<T>::kill(); + CurrentPhase::<T>::put(ConsensusPhase::PowMining); + Self::deposit_event(Event::ValidationTimedOut { block_number }); + } } -/// Default weight info for the pallet pub trait WeightInfo { - fn submit_block() -> Weight; - fn stake() -> Weight; - fn unstake() -> Weight; - fn validate_block() -> Weight; - fn report_misbehavior() -> Weight; - fn register_pqc_key() -> Weight; + fn submit_block() -> Weight; + fn stake() -> Weight; + fn unstake() -> Weight; + fn validate_block() -> Weight; + fn report_misbehavior() -> Weight; + fn register_pq_readiness(metadata_len: u32) -> Weight; + fn attest_pq_readiness(proof_len: u32) -> Weight; + fn remove_pq_readiness() -> Weight; + fn register_ml_dsa_key(key_len: u32) -> Weight; + fn verify_pq_signature(sig_len: u32) -> Weight; } -/// Default implementation of WeightInfo impl WeightInfo for () { - fn submit_block() -> Weight { Weight::from_parts(10_000, 0) } - fn stake() -> Weight { Weight::from_parts(10_000, 0) } - fn unstake() -> Weight { Weight::from_parts(10_000, 0) } - fn validate_block() -> Weight { Weight::from_parts(10_000, 0) } - fn report_misbehavior() -> Weight { Weight::from_parts(10_000, 0) } - fn register_pqc_key() -> Weight { Weight::from_parts(10_000, 0) } + fn submit_block() -> Weight { + Weight::from_parts(10_000, 0) + } + + fn stake() -> Weight { + Weight::from_parts(10_000, 0) + } + + fn unstake() -> Weight { + Weight::from_parts(10_000, 0) + } + + fn validate_block() -> Weight { + Weight::from_parts(10_000, 0) + } + + fn report_misbehavior() -> Weight { + Weight::from_parts(10_000, 0) + } + + fn register_pq_readiness(metadata_len: u32) -> Weight { + Weight::from_parts(50_000, 3_000) + .saturating_add(Weight::from_parts(1_500, 0).saturating_mul(metadata_len.into())) + } + + fn attest_pq_readiness(proof_len: u32) -> Weight { + Weight::from_parts(80_000, 6_000) + .saturating_add(Weight::from_parts(2_000, 0).saturating_mul(proof_len.into())) + } + + fn remove_pq_readiness() -> Weight { + Weight::from_parts(45_000, 4_000) + } + + fn register_ml_dsa_key(key_len: u32) -> Weight { + Weight::from_parts(60_000, 3_000) + .saturating_add(Weight::from_parts(200, 0).saturating_mul(key_len.into())) + } + + fn verify_pq_signature(sig_len: u32) -> Weight { + // ML-DSA-87 verification dominates; conservative placeholder until benchmarked. + Weight::from_parts(8_000_000, 0) + .saturating_add(Weight::from_parts(300, 0).saturating_mul(sig_len.into())) + } } diff --git a/pallets/pallet-ghost-consensus/src/mock.rs b/pallets/pallet-ghost-consensus/src/mock.rs index e61bb84..23ca8a6 100644 --- a/pallets/pallet-ghost-consensus/src/mock.rs +++ b/pallets/pallet-ghost-consensus/src/mock.rs @@ -1,135 +1,177 @@ -//! Test mock for Ghost Consensus Pallet +//! Test mock for the Ghost consensus pallet. use frame_support::{ - construct_runtime, derive_impl, parameter_types, - traits::{ConstU128, ConstU32, ConstU64, ConstU8}, + construct_runtime, derive_impl, parameter_types, + traits::{ConstU128, ConstU64}, }; -use frame_system as system; use pallet_balances; use sp_core::H256; -use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, -}; +use sp_runtime::{traits::BlakeTwo256, BuildStorage}; use crate as pallet_ghost_consensus; +use crate::types::{GenesisHeaderInit, GhostBlockHeader}; type Block = frame_system::mocking::MockBlock<Test>; -frame_support::construct_runtime!( - pub enum Test { - System: frame_system, - Balances: pallet_balances, - GhostConsensus: pallet_ghost_consensus, - } +construct_runtime!( + pub enum Test { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + GhostConsensus: pallet_ghost_consensus, + } ); -#[derive_impl(frame_system::config_with_system_defaults as frame_system::DefaultConfig)] +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { - type Block = Block; - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type RuntimeEvent = RuntimeEvent; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup<Self::AccountId>; - type MaxConsumers = ConstU32<16>; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type AccountData = pallet_balances::AccountData<u128>; } -#[derive_impl(pallet_balances::config_with_system_defaults as pallet_balances::DefaultConfig)] +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for Test { - type MaxLocks = ConstU32<50>; - type MaxReserves = ConstU32<50>; - type ReserveIdentifier = [u8; 8]; + type Balance = u128; + type AccountStore = System; + type ExistentialDeposit = ConstU128<1>; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<1>; + type WeightInfo = (); } parameter_types! { - pub const BlockReward: u128 = 10_000_000_000_000_000_000; // 10 GHOST tokens - pub const MinStake: u128 = 1_000_000_000_000_000_000; // 1 GHOST token - pub const MaxDowntimeBlocks: u32 = 100; - pub const DoubleSignSlashPercentage: u8 = 100; // 100% slash for double signing - pub const InvalidBlockSlashPercentage: u8 = 50; // 50% slash for invalid block - pub const DowntimeSlashPercentage: u8 = 10; // 10% slash for downtime - pub const GhostPalletId: frame_support::PalletId = frame_support::PalletId(*b"py/ghost"); + pub const BlockReward: u128 = 10_000_000_000_000_000_000; + pub const TargetBlockTime: u64 = 5_000; + pub const DifficultyAdjustmentPeriod: u32 = 20; + pub const MinStake: u128 = 1_000_000_000_000_000_000; + pub const MaxDowntimeBlocks: u32 = 100; + pub const MaxValidationBlocks: u32 = 20; + pub const MaxValidators: u32 = 4; + pub const MaxSlashingRecords: u32 = 16; + pub const DoubleSignSlashPercentage: u8 = 100; + pub const InvalidBlockSlashPercentage: u8 = 50; + pub const DowntimeSlashPercentage: u8 = 10; + pub const GhostPalletId: frame_support::PalletId = frame_support::PalletId(*b"py/ghost"); } impl pallet_ghost_consensus::Config for Test { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); - type BlockReward = BlockReward; - type MinStake = MinStake; - type MaxDowntimeBlocks = MaxDowntimeBlocks; - type DoubleSignSlashPercentage = DoubleSignSlashPercentage; - type InvalidBlockSlashPercentage = InvalidBlockSlashPercentage; - type DowntimeSlashPercentage = DowntimeSlashPercentage; - type PalletId = GhostPalletId; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type TargetBlockTime = TargetBlockTime; + type DifficultyAdjustmentPeriod = DifficultyAdjustmentPeriod; + type BlockReward = BlockReward; + type MinStake = MinStake; + type MaxDowntimeBlocks = MaxDowntimeBlocks; + type MaxValidationBlocks = MaxValidationBlocks; + type MaxValidators = MaxValidators; + type MaxSlashingRecords = MaxSlashingRecords; + type DoubleSignSlashPercentage = DoubleSignSlashPercentage; + type InvalidBlockSlashPercentage = InvalidBlockSlashPercentage; + type DowntimeSlashPercentage = DowntimeSlashPercentage; + type PalletId = GhostPalletId; } -pub struct ExtBuilder; +pub struct ExtBuilder { + balances: Vec<(u64, u128)>, + genesis_header: Option<GenesisHeaderInit>, + validator_stakes: Vec<(u64, u128)>, +} impl Default for ExtBuilder { - fn default() -> Self { - ExtBuilder - } + fn default() -> Self { + Self { + balances: vec![ + (1, 100_000_000_000_000_000_000), + (2, 100_000_000_000_000_000_000), + (3, 100_000_000_000_000_000_000), + (4, 50_000_000_000_000_000_000), + (5, 50_000_000_000_000_000_000), + (6, 50_000_000_000_000_000_000), + ], + genesis_header: None, + validator_stakes: Vec::new(), + } + } } impl ExtBuilder { - pub fn build(self) -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::<Test>::default() - .build_storage() - .unwrap(); - - pallet_balances::GenesisConfig::<Test> { - balances: vec![ - (1, 100_000_000_000_000_000_000), // Alice: 100 GHOST - (2, 100_000_000_000_000_000_000), // Bob: 100 GHOST - (3, 100_000_000_000_000_000_000), // Charlie: 100 GHOST - (4, 50_000_000_000_000_000_000), // Dave: 50 GHOST - (5, 25_000_000_000_000_000_000), // Eve: 25 GHOST - ], - } - .assimilate_storage(&mut t) - .unwrap(); - - pallet_ghost_consensus::Pallet::<Test>::on_initialize(0); - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| { - // Initialize storage values for tests - pallet_ghost_consensus::Difficulty::<Test>::put(1_000_000_000_000u64); // Initial difficulty - pallet_ghost_consensus::CurrentPhase::<Test>::put(pallet_ghost_consensus::types::ConsensusPhase::PowMining); - }); - ext - } + pub fn with_balances(mut self, balances: Vec<(u64, u128)>) -> Self { + self.balances = balances; + self + } + + pub fn with_genesis_header(mut self, header: GhostBlockHeader) -> Self { + let header_init: GenesisHeaderInit = ( + header.number, + header.parent_hash, + header.state_root, + header.extrinsics_root, + header.nonce, + header.difficulty, + header.validator_signature, + ); + self.genesis_header = Some(header_init); + self + } + + pub fn with_validator_stakes(mut self, validator_stakes: Vec<(u64, u128)>) -> Self { + self.validator_stakes = validator_stakes; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::<Test>::default() + .build_storage() + .unwrap(); + + pallet_balances::GenesisConfig::<Test> { + balances: self.balances, + } + .assimilate_storage(&mut storage) + .unwrap(); + + pallet_ghost_consensus::GenesisConfig::<Test> { + genesis_header: self.genesis_header, + validator_stakes: self.validator_stakes, + } + .assimilate_storage(&mut storage) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(storage); + ext.execute_with(|| System::set_block_number(1)); + ext + } } -/// Helper function to run a test with the ExtBuilder -pub fn run_test<T>(f: impl FnOnce() -> T) -> T { - ExtBuilder::default().build().execute_with(f) +pub fn run_test<T>(test: impl FnOnce() -> T) -> T { + ExtBuilder::default().build().execute_with(test) } -/// Helper to create a valid block header for testing -pub fn create_block_header(number: u32, nonce: u64) -> pallet_ghost_consensus::types::GhostBlockHeader { - use sp_runtime::traits::Hash; - - let parent_hash = if number == 0 { - H256::zero() - } else { - BlakeTwo256::hash_of(&(number - 1)) - }; - - pallet_ghost_consensus::types::GhostBlockHeader { - number, - parent_hash, - state_root: BlakeTwo256::hash_of(&(number, "state")), - extrinsics_root: BlakeTwo256::hash_of(&(number, "extrinsics")), - nonce, - difficulty: 1_000_000_000_000u64, - validator_signature: None, - } +pub fn create_block_header( + number: u32, + nonce: u64, +) -> pallet_ghost_consensus::types::GhostBlockHeader { + use sp_runtime::traits::Hash; + + let parent_hash = if number == 0 { + H256::zero() + } else { + BlakeTwo256::hash_of(&(number - 1, "parent")) + }; + + pallet_ghost_consensus::types::GhostBlockHeader { + number, + parent_hash, + state_root: BlakeTwo256::hash_of(&(number, "state")), + extrinsics_root: BlakeTwo256::hash_of(&(number, "extrinsics")), + nonce, + difficulty: 1_000_000_000_000, + validator_signature: None, + } } diff --git a/pallets/pallet-ghost-consensus/src/pq_verify.rs b/pallets/pallet-ghost-consensus/src/pq_verify.rs new file mode 100644 index 0000000..bb8f849 --- /dev/null +++ b/pallets/pallet-ghost-consensus/src/pq_verify.rs @@ -0,0 +1,210 @@ +//! Real post-quantum signature verification (ML-DSA / NIST FIPS 204). +//! +//! "Dilithium-5" is standardized as **ML-DSA-87** (FIPS 204, security category 5). +//! This module exposes deterministic, panic-free, `no_std`-compatible verification +//! that runs directly inside the Substrate Wasm runtime. +//! +//! Key generation and signing require randomness and therefore live only behind +//! `#[cfg(test)]` here (and in off-chain node tooling). The runtime itself only ever +//! *verifies* — so the production code path pulls no RNG/`getrandom` dependency. +//! +//! This module is the **only** place that touches the `fips204` crate API directly; +//! the rest of the pallet calls [`verify_ml_dsa`] / [`validate_ml_dsa_pk`], which keeps +//! the external API surface isolated behind a stable wrapper. + +use crate::types::PqAlgorithm; +use fips204::traits::{SerDes, Verifier}; + +/// Public-key byte length for ML-DSA-44 (Dilithium-2, NIST level 2). +pub const ML_DSA_44_PK_LEN: usize = fips204::ml_dsa_44::PK_LEN; +/// Public-key byte length for ML-DSA-65 (Dilithium-3, NIST level 3). +pub const ML_DSA_65_PK_LEN: usize = fips204::ml_dsa_65::PK_LEN; +/// Public-key byte length for ML-DSA-87 (Dilithium-5, NIST level 5). +pub const ML_DSA_87_PK_LEN: usize = fips204::ml_dsa_87::PK_LEN; + +/// Signature byte length for ML-DSA-44. +pub const ML_DSA_44_SIG_LEN: usize = fips204::ml_dsa_44::SIG_LEN; +/// Signature byte length for ML-DSA-65. +pub const ML_DSA_65_SIG_LEN: usize = fips204::ml_dsa_65::SIG_LEN; +/// Signature byte length for ML-DSA-87. +pub const ML_DSA_87_SIG_LEN: usize = fips204::ml_dsa_87::SIG_LEN; + +/// Largest ML-DSA public key (ML-DSA-87). Bounds on-chain key storage. +pub const MAX_ML_DSA_PK_LEN: u32 = ML_DSA_87_PK_LEN as u32; +/// Largest ML-DSA signature (ML-DSA-87). Bounds on-chain signature inputs. +pub const MAX_ML_DSA_SIG_LEN: u32 = ML_DSA_87_SIG_LEN as u32; + +/// `true` if `algorithm` is one of the ML-DSA parameter sets this module verifies. +pub fn is_ml_dsa(algorithm: &PqAlgorithm) -> bool { + matches!( + algorithm, + PqAlgorithm::MlDsa44 | PqAlgorithm::MlDsa65 | PqAlgorithm::MlDsa87 + ) +} + +/// Expected public-key length for `algorithm`, or `None` if it is not ML-DSA. +pub fn ml_dsa_pk_len(algorithm: &PqAlgorithm) -> Option<usize> { + match algorithm { + PqAlgorithm::MlDsa44 => Some(ML_DSA_44_PK_LEN), + PqAlgorithm::MlDsa65 => Some(ML_DSA_65_PK_LEN), + PqAlgorithm::MlDsa87 => Some(ML_DSA_87_PK_LEN), + _ => None, + } +} + +/// Expected signature length for `algorithm`, or `None` if it is not ML-DSA. +pub fn ml_dsa_sig_len(algorithm: &PqAlgorithm) -> Option<usize> { + match algorithm { + PqAlgorithm::MlDsa44 => Some(ML_DSA_44_SIG_LEN), + PqAlgorithm::MlDsa65 => Some(ML_DSA_65_SIG_LEN), + PqAlgorithm::MlDsa87 => Some(ML_DSA_87_SIG_LEN), + _ => None, + } +} + +/// Verify an ML-DSA (FIPS 204) signature. +/// +/// Deterministic, allocation-free, and never panics: any length mismatch, malformed +/// key/signature, oversized context, or non-ML-DSA algorithm returns `false`. Returns +/// `true` only for a cryptographically valid signature of `message` under `public_key` +/// for the given `ctx` (FIPS 204 context string, max 255 bytes). +pub fn verify_ml_dsa( + algorithm: &PqAlgorithm, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ctx: &[u8], +) -> bool { + if ctx.len() > 255 { + return false; + } + match algorithm { + PqAlgorithm::MlDsa44 => verify_44(public_key, message, signature, ctx), + PqAlgorithm::MlDsa65 => verify_65(public_key, message, signature, ctx), + PqAlgorithm::MlDsa87 => verify_87(public_key, message, signature, ctx), + _ => false, + } +} + +/// Structurally validate an ML-DSA public key: correct length for the algorithm and a +/// decodable FIPS 204 encoding. Returns `false` for non-ML-DSA algorithms. +pub fn validate_ml_dsa_pk(algorithm: &PqAlgorithm, public_key: &[u8]) -> bool { + match algorithm { + PqAlgorithm::MlDsa44 => decode_pk_44(public_key).is_some(), + PqAlgorithm::MlDsa65 => decode_pk_65(public_key).is_some(), + PqAlgorithm::MlDsa87 => decode_pk_87(public_key).is_some(), + _ => false, + } +} + +// Generate a decoder + verifier pair for each parameter set. Keeping this in a macro +// guarantees the three sets stay byte-for-byte identical in logic. +macro_rules! impl_param_set { + ($verify:ident, $decode_pk:ident, $m:ident, $pk_len:expr, $sig_len:expr) => { + fn $decode_pk(pk: &[u8]) -> Option<fips204::$m::PublicKey> { + let arr: [u8; $pk_len] = pk.try_into().ok()?; + fips204::$m::PublicKey::try_from_bytes(arr).ok() + } + + fn $verify(pk: &[u8], msg: &[u8], sig: &[u8], ctx: &[u8]) -> bool { + let public = match $decode_pk(pk) { + Some(p) => p, + None => return false, + }; + let sig_arr: [u8; $sig_len] = match sig.try_into() { + Ok(a) => a, + Err(_) => return false, + }; + public.verify(msg, &sig_arr, ctx) + } + }; +} + +impl_param_set!(verify_44, decode_pk_44, ml_dsa_44, ML_DSA_44_PK_LEN, ML_DSA_44_SIG_LEN); +impl_param_set!(verify_65, decode_pk_65, ml_dsa_65, ML_DSA_65_PK_LEN, ML_DSA_65_SIG_LEN); +impl_param_set!(verify_87, decode_pk_87, ml_dsa_87, ML_DSA_87_PK_LEN, ML_DSA_87_SIG_LEN); + +#[cfg(test)] +mod tests { + use super::*; + use fips204::traits::Signer; + + // Dilithium-5 (ML-DSA-87) is the chain default; exercise the full path. + #[test] + fn ml_dsa_87_dilithium5_roundtrip_and_tamper_detection() { + let (pk, sk) = fips204::ml_dsa_87::try_keygen().expect("ml-dsa-87 keygen"); + let msg = b"ghost canonical block header bytes"; + let ctx = b"ghost-validator-v1"; + let sig = sk.try_sign(msg, ctx).expect("ml-dsa-87 sign"); + + let pk_bytes = pk.into_bytes(); + assert_eq!(pk_bytes.len(), ML_DSA_87_PK_LEN); + assert_eq!(sig.len(), ML_DSA_87_SIG_LEN); + + // Valid signature verifies. + assert!(verify_ml_dsa(&PqAlgorithm::MlDsa87, &pk_bytes, msg, &sig, ctx)); + + // Tampered message must fail. + assert!(!verify_ml_dsa(&PqAlgorithm::MlDsa87, &pk_bytes, b"different message", &sig, ctx)); + + // Wrong context must fail. + assert!(!verify_ml_dsa(&PqAlgorithm::MlDsa87, &pk_bytes, msg, &sig, b"wrong-ctx")); + + // Bit-flipped signature must fail. + let mut bad_sig = sig; + bad_sig[100] ^= 0xFF; + assert!(!verify_ml_dsa(&PqAlgorithm::MlDsa87, &pk_bytes, msg, &bad_sig, ctx)); + } + + #[test] + fn ml_dsa_44_and_65_roundtrip() { + let (pk, sk) = fips204::ml_dsa_44::try_keygen().unwrap(); + let sig = sk.try_sign(b"msg-44", b"").unwrap(); + assert!(verify_ml_dsa(&PqAlgorithm::MlDsa44, &pk.into_bytes(), b"msg-44", &sig, b"")); + + let (pk, sk) = fips204::ml_dsa_65::try_keygen().unwrap(); + let sig = sk.try_sign(b"msg-65", b"").unwrap(); + assert!(verify_ml_dsa(&PqAlgorithm::MlDsa65, &pk.into_bytes(), b"msg-65", &sig, b"")); + } + + #[test] + fn wrong_key_fails() { + let (_pk1, sk1) = fips204::ml_dsa_87::try_keygen().unwrap(); + let (pk2, _sk2) = fips204::ml_dsa_87::try_keygen().unwrap(); + let sig = sk1.try_sign(b"m", b"").unwrap(); + // Signature from key 1 must not verify under key 2. + assert!(!verify_ml_dsa(&PqAlgorithm::MlDsa87, &pk2.into_bytes(), b"m", &sig, b"")); + } + + #[test] + fn rejects_bad_lengths_and_non_ml_dsa() { + // Empty public key. + assert!(!verify_ml_dsa(&PqAlgorithm::MlDsa87, &[], b"m", &[0u8; ML_DSA_87_SIG_LEN], b"")); + // Non-ML-DSA algorithm. + assert!(!verify_ml_dsa( + &PqAlgorithm::Unknown, + &[0u8; ML_DSA_87_PK_LEN], + b"m", + &[0u8; ML_DSA_87_SIG_LEN], + b"", + )); + // Oversized context. + let (pk, _sk) = fips204::ml_dsa_87::try_keygen().unwrap(); + assert!(!verify_ml_dsa( + &PqAlgorithm::MlDsa87, + &pk.into_bytes(), + b"m", + &[0u8; ML_DSA_87_SIG_LEN], + &[0u8; 256], + )); + } + + #[test] + fn validate_pk_accepts_real_key_rejects_wrong_length() { + let (pk, _sk) = fips204::ml_dsa_87::try_keygen().unwrap(); + let pk_bytes = pk.into_bytes(); + assert!(validate_ml_dsa_pk(&PqAlgorithm::MlDsa87, &pk_bytes)); + assert!(!validate_ml_dsa_pk(&PqAlgorithm::MlDsa87, &[0u8; 16])); + assert!(!validate_ml_dsa_pk(&PqAlgorithm::Falcon512, &pk_bytes)); + } +} diff --git a/pallets/pallet-ghost-consensus/src/rpc.rs b/pallets/pallet-ghost-consensus/src/rpc.rs deleted file mode 100644 index 85f0158..0000000 --- a/pallets/pallet-ghost-consensus/src/rpc.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! RPC interface for Ghost Consensus Pallet - -use codec::Codec; -use jsonrpsee::{ - core::{async_trait, RpcResult}, - proc_macros::rpc, - types::error::{ErrorObject, ErrorObjectOwned}, -}; -use sp_api::ProvideRuntimeApi; -use sp_blockchain::HeaderBackend; -use sp_runtime::traits::Block as BlockT; -use std::sync::Arc; - -/// Ghost Consensus RPC API -#[rpc(client, server)] -pub trait GhostConsensusApi<BlockHash, AccountId, Balance> { - /// Get current mining difficulty - #[method(name = "ghost_getDifficulty")] - fn get_difficulty(&self, at: Option<BlockHash>) -> RpcResult<u64>; - - /// Get current consensus phase - #[method(name = "ghost_getCurrentPhase")] - fn get_current_phase(&self, at: Option<BlockHash>) -> RpcResult<String>; - - /// Get validator stake - #[method(name = "ghost_getValidatorStake")] - fn get_validator_stake( - &self, - validator: AccountId, - at: Option<BlockHash>, - ) -> RpcResult<Option<Balance>>; - - /// Get all validators - #[method(name = "ghost_getAllValidators")] - fn get_all_validators(&self, at: Option<BlockHash>) -> RpcResult<Vec<(AccountId, Balance)>>; - - /// Get slashing records - #[method(name = "ghost_getSlashingRecords")] - fn get_slashing_records(&self, at: Option<BlockHash>) -> RpcResult<u32>; -} - -/// Runtime API definition for Ghost Consensus -sp_api::decl_runtime_apis! { - pub trait GhostConsensusRuntimeApi<AccountId, Balance> - where - AccountId: Codec, - Balance: Codec, - { - /// Get current mining difficulty - fn get_difficulty() -> u64; - - /// Get current consensus phase - fn get_current_phase() -> u8; - - /// Get validator stake - fn get_validator_stake(validator: AccountId) -> Option<Balance>; - - /// Get all validators with their stakes - fn get_all_validators() -> Vec<(AccountId, Balance)>; - - /// Get number of slashing records - fn get_slashing_records_count() -> u32; - } -} - -/// Implementation of Ghost Consensus RPC API -pub struct GhostConsensusRpc<C, Block> { - client: Arc<C>, - _marker: std::marker::PhantomData<Block>, -} - -impl<C, Block> GhostConsensusRpc<C, Block> { - /// Create new instance - pub fn new(client: Arc<C>) -> Self { - Self { client, _marker: Default::default() } - } -} - -#[async_trait] -impl<C, Block, AccountId, Balance> GhostConsensusApiServer<Block::Hash, AccountId, Balance> - for GhostConsensusRpc<C, Block> -where - Block: BlockT, - C: Send + Sync + 'static + ProvideRuntimeApi<Block> + HeaderBackend<Block>, - C::Api: GhostConsensusRuntimeApi<Block, AccountId, Balance>, - AccountId: Codec, - Balance: Codec, -{ - fn get_difficulty(&self, at: Option<Block::Hash>) -> RpcResult<u64> { - let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - api.get_difficulty(at).map_err(runtime_error_into_rpc_error) - } - - fn get_current_phase(&self, at: Option<Block::Hash>) -> RpcResult<String> { - let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - let phase_num = api.get_current_phase(at).map_err(runtime_error_into_rpc_error)?; - - let phase_name = match phase_num { - 0 => "PowMining", - 1 => "PosValidation", - 2 => "Finalization", - _ => "Unknown", - }; - - Ok(phase_name.to_string()) - } - - fn get_validator_stake( - &self, - validator: AccountId, - at: Option<Block::Hash>, - ) -> RpcResult<Option<Balance>> { - let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - api.get_validator_stake(at, validator).map_err(runtime_error_into_rpc_error) - } - - fn get_all_validators(&self, at: Option<Block::Hash>) -> RpcResult<Vec<(AccountId, Balance)>> { - let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - api.get_all_validators(at).map_err(runtime_error_into_rpc_error) - } - - fn get_slashing_records(&self, at: Option<Block::Hash>) -> RpcResult<u32> { - let api = self.client.runtime_api(); - let at = at.unwrap_or_else(|| self.client.info().best_hash); - - api.get_slashing_records_count(at).map_err(runtime_error_into_rpc_error) - } -} - -/// Convert runtime error to RPC error -fn runtime_error_into_rpc_error(err: impl std::fmt::Display) -> ErrorObjectOwned { - ErrorObject::owned(1, "Runtime error", Some(err.to_string())) -} diff --git a/pallets/pallet-ghost-consensus/src/tests.rs b/pallets/pallet-ghost-consensus/src/tests.rs index 3ecde6c..5d8d589 100644 --- a/pallets/pallet-ghost-consensus/src/tests.rs +++ b/pallets/pallet-ghost-consensus/src/tests.rs @@ -1,426 +1,865 @@ -//! Unit tests for Ghost Consensus Pallet +//! Unit tests for the Ghost consensus pallet. use super::*; -use crate::mock::*; -use crate::types::*; -use frame_support::{assert_err, assert_ok}; -use sp_core::H256; +use crate::{mock::*, types::*}; +use codec::{Decode, Encode}; +use frame_support::{assert_err, assert_ok, traits::Hooks, BoundedVec}; use sp_runtime::traits::{BlakeTwo256, Hash}; -#[test] -fn test_genesis_config() { - run_test(|| { - // Check initial difficulty is set - let difficulty = Difficulty::<Test>::get(); - assert_eq!(difficulty, 1_000_000_000_000u64); - - // Check initial phase is PoW mining - let phase = CurrentPhase::<Test>::get(); - assert_eq!(phase, ConsensusPhase::PowMining); - }); +fn insert_genesis_header() -> GhostBlockHeader { + let header = create_block_header(0, 0); + BlockHeaders::<Test>::insert(0, header.clone()); + header } -#[test] -fn test_difficulty_adjustment_increase() { - run_test(|| { - let current_difficulty = 1_000_000u64; - let actual_block_time = 3u64; // 3 seconds (too fast) - let target_block_time = 5u64; // 5 seconds target +fn mine_test_header(number: u32, parent: &GhostBlockHeader) -> GhostBlockHeader { + // Conventional difficulty: higher = harder. 16 means target = U256::MAX/16 + // (~1 in 16 hashes pass), so a valid nonce is found quickly and deterministically. + Difficulty::<Test>::put(16); + let mut header = create_block_header(number, 0); + header.parent_hash = BlakeTwo256::hash_of(parent); + header.difficulty = Difficulty::<Test>::get(); + + for nonce in 0..10_000 { + header.nonce = nonce; + if functions::verify_pow_enhanced(&header, header.difficulty) { + return header; + } + } + + panic!("failed to find a valid nonce for test block"); +} - let new_difficulty = functions::calculate_difficulty_adjustment::<Test>( - current_difficulty, - actual_block_time, - target_block_time, - ); +fn sample_pq_metadata() -> PqReadinessMetadata<u64> { + PqReadinessMetadata { + version: 2, + algorithm: PqAlgorithm::MlDsa65, + proof_kind: PqProofKind::Attestation, + key_strength_bits: 192, + claimed_nist_level: Some(3), + issued_at: Some(12), + expires_at: Some(144), + public_key_commitment: sp_core::H256::repeat_byte(0xAB), + metadata_hash: Some(sp_core::H256::repeat_byte(0xCD)), + flags: 0b1010_0101, + } +} - // Difficulty should increase since blocks are too fast - assert!(new_difficulty > current_difficulty); - // Should be roughly 1.67x harder (5/3 = 1.67) - assert_eq!(new_difficulty, 1_666_666u64); - }); +fn sample_pq_proof() -> DefaultPqProof<u64> { + DefaultPqProof::<u64> { + algorithm: PqAlgorithm::MlDsa65, + proof_kind: PqProofKind::Attestation, + submitted_at: 42, + statement_hash: sp_core::H256::repeat_byte(0x11), + public_key_commitment: sp_core::H256::repeat_byte(0xAB), + proof: vec![7u8; 128].try_into().expect("proof fits in bound"), + context: b"validator-2 attests readiness" + .to_vec() + .try_into() + .expect("context fits in bound"), + auxiliary_hash: Some(sp_core::H256::repeat_byte(0x33)), + } } #[test] -fn test_difficulty_adjustment_decrease() { - run_test(|| { - let current_difficulty = 1_000_000u64; - let actual_block_time = 10u64; // 10 seconds (too slow) - let target_block_time = 5u64; // 5 seconds target - - let new_difficulty = functions::calculate_difficulty_adjustment::<Test>( - current_difficulty, - actual_block_time, - target_block_time, - ); - - // Difficulty should decrease since blocks are too slow - assert!(new_difficulty < current_difficulty); - // Should be roughly 0.5x easier (5/10 = 0.5) - assert_eq!(new_difficulty, 500_000u64); - }); +fn genesis_config_is_initialized() { + run_test(|| { + assert_eq!(Difficulty::<Test>::get(), 100_000u64); + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PowMining); + assert_eq!(ValidatorCount::<Test>::get(), 0); + }); } #[test] -fn test_pow_verification_enhanced() { - run_test(|| { - // Create a block header with a low difficulty to make testing easier - let mut header = create_block_header(1, 0); - header.difficulty = u64::MAX; // Very easy difficulty - - // This should pass with a high difficulty - assert!(functions::verify_pow_enhanced(&header, u64::MAX)); - - // Test with impossible difficulty (should fail) - assert!(!functions::verify_pow_enhanced(&header, 1)); - }); +fn difficulty_adjustment_moves_with_block_time() { + run_test(|| { + let increased = functions::calculate_difficulty_adjustment::<Test>(1_000_000, 3, 5); + let decreased = functions::calculate_difficulty_adjustment::<Test>(1_000_000, 10, 5); + + assert!(increased > 1_000_000); + assert!(decreased < 1_000_000); + }); } #[test] -fn test_staking_basic() { - run_test(|| { - let staker = 1u64; // Alice - let stake_amount = 10_000_000_000_000_000_000u128; // 10 GHOST - - // Stake tokens - assert_ok!(GhostConsensus::stake(RuntimeOrigin::signed(staker), stake_amount)); - - // Check stake was recorded - let recorded_stake = ValidatorStakes::<Test>::get(staker); - assert!(recorded_stake.is_some()); - assert_eq!(recorded_stake.unwrap(), stake_amount); - }); +fn genesis_bootstrap_caps_and_filters_validators() { + let genesis_header = create_block_header(0, 7); + ExtBuilder::default() + .with_genesis_header(genesis_header.clone()) + .with_validator_stakes(vec![ + (1, MinStake::get()), + (2, MinStake::get() * 2), + (3, MinStake::get() - 1), + (4, MinStake::get()), + (5, MinStake::get()), + (6, MinStake::get()), + ]) + .build() + .execute_with(|| { + assert_eq!(BlockHeaders::<Test>::get(0), Some(genesis_header)); + assert_eq!(ValidatorCount::<Test>::get(), 3); + assert_eq!(ValidatorStakes::<Test>::get(1), Some(MinStake::get())); + assert_eq!(ValidatorStakes::<Test>::get(2), Some(MinStake::get() * 2)); + assert_eq!(ValidatorStakes::<Test>::get(3), None); + assert_eq!(ValidatorStakes::<Test>::get(4), Some(MinStake::get())); + assert_eq!(ValidatorStakes::<Test>::get(5), None); + assert_eq!(ValidatorStakes::<Test>::get(6), None); + // Genesis now backs each accepted validator stake with real tokens in the + // pallet account (validators 1, 2, 4 => MinStake + 2*MinStake + MinStake). + assert_eq!( + Balances::free_balance(GhostConsensus::account_id()), + MinStake::get() * 4 + ); + assert_eq!(LastActiveBlock::<Test>::get(1), 0); + assert_eq!(LastActiveBlock::<Test>::get(5), 0); + }); } #[test] -fn test_staking_below_minimum() { - run_test(|| { - let staker = 1u64; // Alice - let stake_amount = 500_000_000_000_000_000u128; // 0.5 GHOST (below minimum) - - // Should fail because stake is below minimum - assert_err!( - GhostConsensus::stake(RuntimeOrigin::signed(staker), stake_amount), - Error::<Test>::InsufficientStake - ); - }); +fn stake_and_unstake_move_balances() { + run_test(|| { + let staker = 1u64; + let amount = 10_000_000_000_000_000_000u128; + let starting_balance = Balances::free_balance(staker); + + assert_ok!(GhostConsensus::stake(RuntimeOrigin::signed(staker), amount)); + assert_eq!(ValidatorStakes::<Test>::get(staker), Some(amount)); + assert_eq!(Balances::free_balance(staker), starting_balance - amount); + assert_eq!(Balances::free_balance(GhostConsensus::account_id()), amount); + + assert_ok!(GhostConsensus::unstake( + RuntimeOrigin::signed(staker), + amount / 2 + )); + assert_eq!(ValidatorStakes::<Test>::get(staker), Some(amount / 2)); + assert_eq!( + Balances::free_balance(GhostConsensus::account_id()), + amount / 2 + ); + }); } #[test] -fn test_staking_multiple_times() { - run_test(|| { - let staker = 1u64; // Alice - let first_stake = 5_000_000_000_000_000_000u128; // 5 GHOST - let second_stake = 3_000_000_000_000_000_000u128; // 3 GHOST - - // First stake - assert_ok!(GhostConsensus::stake(RuntimeOrigin::signed(staker), first_stake)); - - // Second stake - assert_ok!(GhostConsensus::stake(RuntimeOrigin::signed(staker), second_stake)); - - // Check total stake - let total_stake = ValidatorStakes::<Test>::get(staker).unwrap(); - assert_eq!(total_stake, first_stake + second_stake); - }); +fn stake_below_minimum_fails() { + run_test(|| { + assert_err!( + GhostConsensus::stake(RuntimeOrigin::signed(1), 500_000_000_000_000_000u128), + Error::<Test>::InsufficientStake + ); + }); } #[test] -fn test_unstaking_basic() { - run_test(|| { - let staker = 1u64; // Alice - let stake_amount = 10_000_000_000_000_000_000u128; // 10 GHOST - let unstake_amount = 3_000_000_000_000_000_000u128; // 3 GHOST - - // Stake first - assert_ok!(GhostConsensus::stake(RuntimeOrigin::signed(staker), stake_amount)); - - // Unstake partial amount - assert_ok!(GhostConsensus::unstake(RuntimeOrigin::signed(staker), unstake_amount)); - - // Check remaining stake - let remaining_stake = ValidatorStakes::<Test>::get(staker).unwrap(); - assert_eq!(remaining_stake, stake_amount - unstake_amount); - }); +fn validator_selection_returns_weighted_candidate() { + run_test(|| { + let stakers = vec![ + ValidatorStake { + account: 1u64, + stake: 50, + weight: 50, + }, + ValidatorStake { + account: 2u64, + stake: 30, + weight: 30, + }, + ValidatorStake { + account: 3u64, + stake: 20, + weight: 20, + }, + ]; + let seed = sp_core::H256::from_low_u64_be(12345); + let selected = functions::select_pos_validator::<Test>(stakers, seed).unwrap(); + + assert!((1..=3).contains(&selected.validator)); + assert!(selected.weight > 0); + }); } #[test] -fn test_unstaking_without_stake() { - run_test(|| { - let staker = 1u64; // Alice - let unstake_amount = 1_000_000_000_000_000_000u128; // 1 GHOST - - // Try to unstake without staking first - assert_err!( - GhostConsensus::unstake(RuntimeOrigin::signed(staker), unstake_amount), - Error::<Test>::NotAValidator - ); - }); +fn submit_block_transitions_to_pos_validation() { + run_test(|| { + // A staker must exist for there to be a validator to select; with an empty + // staker set the block is finalized immediately instead (see the test below). + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(2), + 20_000_000_000_000_000_000u128 + )); + let genesis = insert_genesis_header(); + let header = mine_test_header(1, &genesis); + + assert_ok!(GhostConsensus::submit_block( + RuntimeOrigin::signed(1), + header.clone() + )); + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PosValidation); + assert_eq!(BlockHeaders::<Test>::get(1), Some(header)); + assert_eq!(BlockMiners::<Test>::get(1), Some(1)); + }); } #[test] -fn test_unstaking_more_than_staked() { - run_test(|| { - let staker = 1u64; // Alice - let stake_amount = 5_000_000_000_000_000_000u128; // 5 GHOST - let unstake_amount = 10_000_000_000_000_000_000u128; // 10 GHOST (more than staked) - - // Stake first - assert_ok!(GhostConsensus::stake(RuntimeOrigin::signed(staker), stake_amount)); - - // Try to unstake more than staked - assert_err!( - GhostConsensus::unstake(RuntimeOrigin::signed(staker), unstake_amount), - Error::<Test>::InsufficientStake - ); - }); +fn submit_block_with_no_stakers_finalizes_immediately() { + run_test(|| { + let genesis = insert_genesis_header(); + let header = mine_test_header(1, &genesis); + let miner = 1u64; + let reward = <Test as Config>::BlockReward::get(); + let miner_before = Balances::free_balance(miner); + + // No stakers -> no validator to select. The block is finalized immediately, + // the miner receives the FULL reward, and the chain stays in PowMining rather + // than stalling in PosValidation until the validation timeout fires. + assert_ok!(GhostConsensus::submit_block( + RuntimeOrigin::signed(miner), + header + )); + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PowMining); + assert_eq!(PendingValidationBlock::<Test>::get(), None); + assert_eq!(PendingValidator::<Test>::get(), None); + assert_eq!(Balances::free_balance(miner), miner_before + reward); + + // A second block can be submitted right away: no stall, no timeout required. + let stored = BlockHeaders::<Test>::get(1).unwrap(); + let header2 = mine_test_header(2, &stored); + assert_ok!(GhostConsensus::submit_block( + RuntimeOrigin::signed(miner), + header2 + )); + assert_eq!(BlockMiners::<Test>::get(2), Some(miner)); + }); } #[test] -fn test_validator_selection_weighted() { - run_test(|| { - // Create validators with different stakes - let stakers = vec![ - ValidatorStake { - account: 1u64, - stake: 50_000_000_000_000_000_000u128, // 50 GHOST - weight: 50, - }, - ValidatorStake { - account: 2u64, - stake: 30_000_000_000_000_000_000u128, // 30 GHOST - weight: 30, - }, - ValidatorStake { - account: 3u64, - stake: 20_000_000_000_000_000_000u128, // 20 GHOST - weight: 20, - }, - ]; - - let seed = H256::from_low_u64_be(12345); - let selection = functions::select_pos_validator::<Test>(stakers, seed); - - assert!(selection.is_some()); - let selected = selection.unwrap(); - assert!(selected.validator >= 1 && selected.validator <= 3); - assert!(selected.weight > 0); - }); +fn validate_block_selects_validator_and_distributes_rewards() { + run_test(|| { + let genesis = insert_genesis_header(); + let header = mine_test_header(1, &genesis); + let miner = 1u64; + let validator = 2u64; + let reward = <Test as Config>::BlockReward::get(); + let reward_split = functions::calculate_block_reward::<Test>(reward); + + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(validator), + 20_000_000_000_000_000_000u128 + )); + let miner_before = Balances::free_balance(miner); + let validator_before = Balances::free_balance(validator); + + assert_ok!(GhostConsensus::submit_block( + RuntimeOrigin::signed(miner), + header + )); + assert_ok!(GhostConsensus::validate_block( + RuntimeOrigin::signed(validator), + 1, + None + )); + + let stored_header = BlockHeaders::<Test>::get(1).unwrap(); + assert!(stored_header.validator_signature.is_some()); + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::Finalization); + assert_eq!( + Balances::free_balance(miner), + miner_before + reward_split.miner_reward + ); + assert_eq!( + Balances::free_balance(validator), + validator_before + reward_split.stakers_reward + ); + }); } #[test] -fn test_validator_selection_empty() { - run_test(|| { - let stakers: Vec<ValidatorStake<u64, u128>> = vec![]; - let seed = H256::from_low_u64_be(12345); - let selection = functions::select_pos_validator::<Test>(stakers, seed); - - assert!(selection.is_none()); - }); +fn report_misbehavior_slashes_stake_and_records_reason() { + run_test(|| { + let validator = 1u64; + let amount = 10_000_000_000_000_000_000u128; + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(validator), + amount + )); + assert_eq!(ValidatorCount::<Test>::get(), 1); + + assert_ok!(GhostConsensus::report_misbehavior( + RuntimeOrigin::signed(2), + validator, + SlashingReason::DoubleSigning, + MisbehaviorEvidence::DoubleSigning { + first_vote: sp_core::H256::repeat_byte(1), + second_vote: sp_core::H256::repeat_byte(2), + }, + )); + + let expected_remaining = amount - ((amount * 100u128) / 100u128); + assert_eq!(ValidatorStakes::<Test>::get(validator), None); + assert_eq!(expected_remaining, 0); + assert_eq!(ValidatorCount::<Test>::get(), 0); + assert!(DoubleSignReports::<Test>::get(validator)); + assert_eq!(SlashingRecords::<Test>::get().len(), 1); + }); } #[test] -fn test_block_reward_calculation() { - run_test(|| { - let total_reward = 10_000_000_000_000_000_000u128; // 10 GHOST - let reward = functions::calculate_block_reward::<Test>(total_reward); - - // 40% to miner - assert_eq!(reward.miner_reward, 4_000_000_000_000_000_000u128); - // 60% to stakers - assert_eq!(reward.stakers_reward, 6_000_000_000_000_000_000u128); - // Total should match - assert_eq!(reward.total, total_reward); - assert_eq!(reward.miner_reward + reward.stakers_reward, total_reward); - }); +fn slashing_requires_reason_matched_evidence() { + run_test(|| { + let validator = 1u64; + let amount = 10_000_000_000_000_000_000u128; + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(validator), + amount + )); + + assert_err!( + GhostConsensus::report_misbehavior( + RuntimeOrigin::signed(2), + validator, + SlashingReason::DoubleSigning, + MisbehaviorEvidence::DoubleSigning { + first_vote: sp_core::H256::repeat_byte(1), + second_vote: sp_core::H256::repeat_byte(1), + }, + ), + Error::<Test>::InvalidEvidence + ); + assert_eq!(ValidatorStakes::<Test>::get(validator), Some(amount)); + }); } #[test] -fn test_block_header_validation_sequence() { - run_test(|| { - let parent_header = create_block_header(0, 100); - let mut child_header = create_block_header(1, 200); - child_header.parent_hash = BlakeTwo256::hash_of(&parent_header); - child_header.difficulty = 1_000_000_000_000u64; - - // Store parent header - BlockHeaders::<Test>::insert(0, parent_header.clone()); - - // This might fail on PoW verification depending on nonce, but should pass other checks - let result = functions::validate_block_header::<Test>(&child_header, &parent_header); - // We expect it might fail on PoW, but not on sequence or parent hash - // Just ensure it runs without panicking - }); +fn downtime_slashing_requires_timeout_evidence() { + run_test(|| { + let validator = 1u64; + let amount = 10_000_000_000_000_000_000u128; + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(validator), + amount + )); + + System::set_block_number(MaxDowntimeBlocks::get().into()); + assert_err!( + GhostConsensus::report_misbehavior( + RuntimeOrigin::signed(2), + validator, + SlashingReason::Downtime, + MisbehaviorEvidence::Downtime, + ), + Error::<Test>::InvalidEvidence + ); + + System::set_block_number((MaxDowntimeBlocks::get() + 2).into()); + assert_ok!(GhostConsensus::report_misbehavior( + RuntimeOrigin::signed(2), + validator, + SlashingReason::Downtime, + MisbehaviorEvidence::Downtime, + )); + + let expected_remaining = amount - ((amount * DowntimeSlashPercentage::get() as u128) / 100); + assert_eq!( + ValidatorStakes::<Test>::get(validator), + Some(expected_remaining) + ); + assert_eq!(ValidatorCount::<Test>::get(), 1); + }); } #[test] -fn test_block_header_validation_wrong_number() { - run_test(|| { - let parent_header = create_block_header(0, 100); - let mut child_header = create_block_header(5, 200); // Wrong number (should be 1) - child_header.parent_hash = BlakeTwo256::hash_of(&parent_header); - - BlockHeaders::<Test>::insert(0, parent_header.clone()); - - assert_err!( - functions::validate_block_header::<Test>(&child_header, &parent_header), - Error::<Test>::InvalidBlockNumber - ); - }); +fn downtime_slashing_skips_when_slashing_records_are_full() { + run_test(|| { + let validator = 1u64; + let amount = 10_000_000_000_000_000_000u128; + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(validator), + amount + )); + + let full_records = (0..MaxSlashingRecords::get()) + .map(|idx| { + ( + validator + idx as u64 + 10, + SlashingReason::Other, + 1u128, + idx as u64, + ) + }) + .collect::<Vec<_>>(); + SlashingRecords::<Test>::put( + BoundedVec::try_from(full_records).expect("records should fill the configured bound"), + ); + + let events_before = System::events().len(); + System::set_block_number((MaxDowntimeBlocks::get() + 11).into()); + GhostConsensus::check_downtime_slashing(); + + assert_eq!(ValidatorStakes::<Test>::get(validator), Some(amount)); + assert_eq!(ValidatorCount::<Test>::get(), 1); + assert_eq!(SlashingRecords::<Test>::get().len(), MaxSlashingRecords::get() as usize); + assert_eq!(System::events().len(), events_before); + }); } #[test] -fn test_block_header_validation_wrong_parent() { - run_test(|| { - let parent_header = create_block_header(0, 100); - let mut child_header = create_block_header(1, 200); - child_header.parent_hash = H256::zero(); // Wrong parent hash - - BlockHeaders::<Test>::insert(0, parent_header.clone()); - - assert_err!( - functions::validate_block_header::<Test>(&child_header, &parent_header), - Error::<Test>::InvalidParentHash - ); - }); +fn register_pq_readiness_stores_metadata_and_emits_event() { + run_test(|| { + let account = 2u64; + let metadata = sample_pq_metadata(); + + assert_ok!(GhostConsensus::register_pq_readiness( + RuntimeOrigin::signed(account), + metadata.clone() + )); + + assert_eq!(PqReadinessRegistry::<Test>::get(account), Some(metadata)); + System::assert_last_event( + Event::PqReadinessRegistered { + account, + algorithm: PqAlgorithm::MlDsa65, + proof_kind: PqProofKind::Attestation, + } + .into(), + ); + }); } #[test] -fn test_phase_transitions() { - run_test(|| { - // Start in PoW mining phase - assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PowMining); +fn register_pq_readiness_rejects_invalid_metadata() { + run_test(|| { + let account = 2u64; + + let mut invalid_version = sample_pq_metadata(); + invalid_version.version = 0; + assert_err!( + GhostConsensus::register_pq_readiness(RuntimeOrigin::signed(account), invalid_version), + Error::<Test>::InvalidPqMetadata + ); + + let mut invalid_algorithm = sample_pq_metadata(); + invalid_algorithm.algorithm = PqAlgorithm::Unknown; + assert_err!( + GhostConsensus::register_pq_readiness( + RuntimeOrigin::signed(account), + invalid_algorithm + ), + Error::<Test>::InvalidPqMetadata + ); + + let mut invalid_window = sample_pq_metadata(); + invalid_window.issued_at = Some(20); + invalid_window.expires_at = Some(19); + assert_err!( + GhostConsensus::register_pq_readiness(RuntimeOrigin::signed(account), invalid_window), + Error::<Test>::InvalidPqMetadata + ); + + assert_eq!(PqReadinessRegistry::<Test>::get(account), None); + }); +} - // Transition to PoS validation - CurrentPhase::<Test>::put(ConsensusPhase::PosValidation); - assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PosValidation); +#[test] +fn pq_readiness_metadata_roundtrips_and_preserves_claims() { + let metadata = sample_pq_metadata(); - // Transition to finalization - CurrentPhase::<Test>::put(ConsensusPhase::Finalization); - assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::Finalization); + let encoded = metadata.encode(); + let decoded = PqReadinessMetadata::<u64>::decode(&mut &encoded[..]) + .expect("metadata should decode after encoding"); - // Back to PoW mining - CurrentPhase::<Test>::put(ConsensusPhase::PowMining); - assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PowMining); - }); + assert_eq!(decoded, metadata); + assert_eq!(decoded.algorithm, PqAlgorithm::MlDsa65); + assert_eq!(decoded.proof_kind, PqProofKind::Attestation); + assert_eq!(decoded.claimed_nist_level, Some(3)); } #[test] -fn test_multiple_validators_staking() { - run_test(|| { - // Multiple validators stake different amounts - let validators = vec![ - (1u64, 10_000_000_000_000_000_000u128), // Alice: 10 GHOST - (2u64, 20_000_000_000_000_000_000u128), // Bob: 20 GHOST - (3u64, 15_000_000_000_000_000_000u128), // Charlie: 15 GHOST - ]; - - for (validator, amount) in validators.iter() { - assert_ok!(GhostConsensus::stake(RuntimeOrigin::signed(*validator), *amount)); - } - - // Verify all stakes are recorded - for (validator, amount) in validators.iter() { - let stake = ValidatorStakes::<Test>::get(validator).unwrap(); - assert_eq!(stake, *amount); - } - }); +fn default_pq_proof_alias_roundtrips_with_attestation_payload() { + let mut proof = sample_pq_proof(); + proof.algorithm = PqAlgorithm::Hybrid; + proof.public_key_commitment = sp_core::H256::repeat_byte(0x22); + + let encoded = proof.encode(); + let decoded = DefaultPqProof::<u64>::decode(&mut &encoded[..]) + .expect("proof should decode after encoding"); + + assert_eq!(decoded, proof); + assert_eq!(decoded.proof.len(), 128); + assert_eq!(decoded.context.as_slice(), b"validator-2 attests readiness"); + assert_eq!(decoded.proof_kind, PqProofKind::Attestation); } #[test] -fn test_last_active_block_tracking() { - run_test(|| { - let validator = 1u64; - let block_number = 100u32; - - LastActiveBlock::<Test>::insert(validator, block_number); - - let last_active = LastActiveBlock::<Test>::get(validator); - assert_eq!(last_active, block_number); - }); +fn attest_pq_readiness_stores_proof_and_emits_event() { + run_test(|| { + let account = 2u64; + let metadata = sample_pq_metadata(); + let proof = sample_pq_proof(); + + assert_ok!(GhostConsensus::register_pq_readiness( + RuntimeOrigin::signed(account), + metadata + )); + assert_ok!(GhostConsensus::attest_pq_readiness( + RuntimeOrigin::signed(account), + proof.clone() + )); + + assert_eq!(PqReadinessAttestations::<Test>::get(account), Some(proof)); + System::assert_last_event( + Event::PqReadinessAttested { + account, + algorithm: PqAlgorithm::MlDsa65, + proof_kind: PqProofKind::Attestation, + statement_hash: sp_core::H256::repeat_byte(0x11), + } + .into(), + ); + }); } #[test] -fn test_difficulty_storage() { - run_test(|| { - let new_difficulty = 5_000_000_000_000u64; - - Difficulty::<Test>::put(new_difficulty); - - let stored_difficulty = Difficulty::<Test>::get(); - assert_eq!(stored_difficulty, new_difficulty); - }); +fn attest_pq_readiness_rejects_missing_or_mismatched_metadata() { + run_test(|| { + let account = 2u64; + let proof = sample_pq_proof(); + + assert_err!( + GhostConsensus::attest_pq_readiness(RuntimeOrigin::signed(account), proof.clone()), + Error::<Test>::PqReadinessNotFound + ); + + let metadata = sample_pq_metadata(); + assert_ok!(GhostConsensus::register_pq_readiness( + RuntimeOrigin::signed(account), + metadata + )); + + let mut mismatched_proof = proof; + mismatched_proof.public_key_commitment = sp_core::H256::repeat_byte(0xFE); + assert_err!( + GhostConsensus::attest_pq_readiness(RuntimeOrigin::signed(account), mismatched_proof), + Error::<Test>::PqMetadataMismatch + ); + + assert_eq!(PqReadinessAttestations::<Test>::get(account), None); + }); } #[test] -fn test_pow_verification_different_algorithms() { - run_test(|| { - let header = create_block_header(1, 12345); - let easy_difficulty = u64::MAX; - let hard_difficulty = 1000u64; - - // Test basic Blake2 - let result_basic = functions::verify_pow(&header, easy_difficulty); - assert!(result_basic); - - // Test enhanced Blake2 - let result_enhanced = functions::verify_pow_enhanced(&header, easy_difficulty); - assert!(result_enhanced); - - // Test SHA-256 - let result_sha = functions::verify_pow_sha256(&header, easy_difficulty); - assert!(result_sha); - - // Test Keccak - let result_keccak = functions::verify_pow_keccak(&header, easy_difficulty); - assert!(result_keccak); +fn remove_pq_readiness_clears_registry_and_attestations() { + run_test(|| { + let account = 2u64; + let metadata = sample_pq_metadata(); + let proof = sample_pq_proof(); + + assert_ok!(GhostConsensus::register_pq_readiness( + RuntimeOrigin::signed(account), + metadata + )); + assert_ok!(GhostConsensus::attest_pq_readiness( + RuntimeOrigin::signed(account), + proof + )); + + assert_ok!(GhostConsensus::remove_pq_readiness(RuntimeOrigin::signed( + account + ))); + + assert_eq!(PqReadinessRegistry::<Test>::get(account), None); + assert_eq!(PqReadinessAttestations::<Test>::get(account), None); + System::assert_last_event(Event::PqReadinessRemoved { account }.into()); + }); +} - // All should fail with very hard difficulty - assert!(!functions::verify_pow(&header, hard_difficulty)); - assert!(!functions::verify_pow_enhanced(&header, hard_difficulty)); - assert!(!functions::verify_pow_sha256(&header, hard_difficulty)); - assert!(!functions::verify_pow_keccak(&header, hard_difficulty)); - }); +#[test] +fn default_pq_proof_alias_enforces_payload_bounds() { + let oversized_proof = vec![1u8; 4097]; + let oversized_context = vec![2u8; 257]; + + assert!( + BoundedVec::<u8, frame_support::traits::ConstU32<4096>>::try_from(oversized_proof).is_err() + ); + assert!( + BoundedVec::<u8, frame_support::traits::ConstU32<256>>::try_from(oversized_context) + .is_err() + ); + assert_eq!(PqAlgorithm::default(), PqAlgorithm::Unknown); + assert_eq!(PqProofKind::default(), PqProofKind::Unknown); } #[test] -fn test_slashing_records_storage() { - run_test(|| { - let validator = 1u64; - let reason = SlashingReason::DoubleSign; - let amount = 5_000_000_000_000_000_000u128; // 5 GHOST - let block_number = 100u32; +fn validator_count_is_bounded() { + run_test(|| { + for account in 1..=4 { + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(account), + 1_000_000_000_000_000_000u128 + )); + } + + assert_err!( + GhostConsensus::stake(RuntimeOrigin::signed(5), 1_000_000_000_000_000_000u128), + Error::<Test>::TooManyValidators + ); + }); +} - let mut records = SlashingRecords::<Test>::get(); - records.push((validator, reason, amount, block_number)); - SlashingRecords::<Test>::put(records); +#[test] +fn existing_validator_can_top_up_at_validator_cap() { + run_test(|| { + for account in 1..=4 { + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(account), + 1_000_000_000_000_000_000u128 + )); + } + + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(1), + 2_000_000_000_000_000_000u128 + )); + + assert_eq!(ValidatorCount::<Test>::get(), 4); + assert_eq!( + ValidatorStakes::<Test>::get(1), + Some(3_000_000_000_000_000_000u128) + ); + }); +} - let stored_records = SlashingRecords::<Test>::get(); - assert_eq!(stored_records.len(), 1); - assert_eq!(stored_records[0].0, validator); - assert_eq!(stored_records[0].2, amount); - }); +#[test] +fn validation_timeout_waits_for_strict_boundary_then_recovers_pow_phase() { + run_test(|| { + // Stake so the block enters PosValidation with a selected validator; the + // validator then never validates, which is exactly the timeout path under test. + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(2), + 20_000_000_000_000_000_000u128 + )); + let genesis = insert_genesis_header(); + let header = mine_test_header(1, &genesis); + + assert_ok!(GhostConsensus::submit_block( + RuntimeOrigin::signed(1), + header + )); + assert_eq!(PhaseStartedAt::<Test>::get(), 1); + + let boundary_block = 1 + MaxValidationBlocks::get(); + System::set_block_number(boundary_block.into()); + GhostConsensus::on_initialize(boundary_block.into()); + + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PosValidation); + assert_eq!(PendingValidationBlock::<Test>::get(), Some(1)); + + let timed_out_block = boundary_block + 1; + System::set_block_number(timed_out_block.into()); + GhostConsensus::on_initialize(timed_out_block.into()); + + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PowMining); + assert_eq!(PendingValidationBlock::<Test>::get(), None); + }); } #[test] -fn test_double_sign_reports() { - run_test(|| { - let validator = 1u64; +fn on_finalize_resets_phase_back_to_pow() { + run_test(|| { + CurrentPhase::<Test>::put(ConsensusPhase::Finalization); + GhostConsensus::on_finalize(1); + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::PowMining); + }); +} - DoubleSignReports::<Test>::insert(validator, true); +#[test] +fn validate_block_enforces_real_ml_dsa_signature_when_registered() { + use fips204::traits::{SerDes, Signer}; + run_test(|| { + let miner = 1u64; + let validator = 2u64; + + assert_ok!(GhostConsensus::stake( + RuntimeOrigin::signed(validator), + 20_000_000_000_000_000_000u128 + )); + + // Register a real ML-DSA-87 ("Dilithium-5") key for the validator. + let (pk, sk) = fips204::ml_dsa_87::try_keygen().expect("keygen"); + let pk_bytes = pk.into_bytes(); + assert_ok!(GhostConsensus::register_ml_dsa_key( + RuntimeOrigin::signed(validator), + PqAlgorithm::MlDsa87, + pk_bytes.to_vec().try_into().expect("pk fits bound"), + )); + + let genesis = insert_genesis_header(); + let header = mine_test_header(1, &genesis); + assert_ok!(GhostConsensus::submit_block( + RuntimeOrigin::signed(miner), + header + )); + assert_eq!(PendingValidator::<Test>::get(), Some(validator)); + + // The validator signs the canonical message: (stored header, validator id). + let stored = BlockHeaders::<Test>::get(1).unwrap(); + let message = ( + stored.number, + stored.parent_hash, + stored.state_root, + stored.extrinsics_root, + stored.nonce, + stored.difficulty, + validator, + ) + .encode(); + let good_sig = sk.try_sign(&message, GHOST_VALIDATOR_CTX).expect("sign"); + + // A registered validator cannot validate without a signature. + assert_err!( + GhostConsensus::validate_block(RuntimeOrigin::signed(validator), 1, None), + Error::<Test>::MlDsaSignatureMissing + ); + + // A tampered signature is rejected by on-chain ML-DSA verification. + let mut bad_sig = good_sig; + bad_sig[10] ^= 0xFF; + assert_err!( + GhostConsensus::validate_block( + RuntimeOrigin::signed(validator), + 1, + Some(bad_sig.to_vec().try_into().unwrap()) + ), + Error::<Test>::MlDsaSignatureInvalid + ); + + // The correct ML-DSA signature validates the block. + assert_ok!(GhostConsensus::validate_block( + RuntimeOrigin::signed(validator), + 1, + Some(good_sig.to_vec().try_into().unwrap()) + )); + assert_eq!(CurrentPhase::<Test>::get(), ConsensusPhase::Finalization); + assert_eq!(BlockValidators::<Test>::get(1), Some(validator)); + }); +} - let is_reported = DoubleSignReports::<Test>::get(validator); - assert!(is_reported); - }); +#[test] +fn verify_pq_signature_extrinsic_checks_real_ml_dsa() { + use fips204::traits::{SerDes, Signer}; + run_test(|| { + let attester = 2u64; + let (pk, sk) = fips204::ml_dsa_87::try_keygen().expect("keygen"); + assert_ok!(GhostConsensus::register_ml_dsa_key( + RuntimeOrigin::signed(attester), + PqAlgorithm::MlDsa87, + pk.into_bytes().to_vec().try_into().unwrap(), + )); + + let message = b"ghost attests a statement".to_vec(); + let ctx = b"attestation-ctx".to_vec(); + let sig = sk.try_sign(&message, &ctx).expect("sign"); + + // A real, valid ML-DSA signature is accepted. + assert_ok!(GhostConsensus::verify_pq_signature( + RuntimeOrigin::signed(attester), + message.clone().try_into().unwrap(), + sig.to_vec().try_into().unwrap(), + ctx.clone().try_into().unwrap(), + )); + + // A bit-flipped signature is rejected. + let mut bad = sig; + bad[5] ^= 0xFF; + assert_err!( + GhostConsensus::verify_pq_signature( + RuntimeOrigin::signed(attester), + message.try_into().unwrap(), + bad.to_vec().try_into().unwrap(), + ctx.try_into().unwrap(), + ), + Error::<Test>::MlDsaSignatureInvalid + ); + }); } #[test] -fn test_invalid_block_reports() { - run_test(|| { - let validator = 1u64; +fn verify_pq_signature_rejects_replay() { + use fips204::traits::{SerDes, Signer}; + run_test(|| { + let attester = 2u64; + let (pk, sk) = fips204::ml_dsa_87::try_keygen().expect("keygen"); + assert_ok!(GhostConsensus::register_ml_dsa_key( + RuntimeOrigin::signed(attester), + PqAlgorithm::MlDsa87, + pk.into_bytes().to_vec().try_into().unwrap(), + )); + + let message = b"replayable statement".to_vec(); + let ctx = b"attestation-ctx".to_vec(); + let sig = sk.try_sign(&message, &ctx).expect("sign"); + + // First submission of a valid attestation is recorded. + assert_ok!(GhostConsensus::verify_pq_signature( + RuntimeOrigin::signed(attester), + message.clone().try_into().unwrap(), + sig.to_vec().try_into().unwrap(), + ctx.clone().try_into().unwrap(), + )); + + // Replaying the exact same valid attestation is rejected by the replay guard. + assert_err!( + GhostConsensus::verify_pq_signature( + RuntimeOrigin::signed(attester), + message.try_into().unwrap(), + sig.to_vec().try_into().unwrap(), + ctx.try_into().unwrap(), + ), + Error::<Test>::AttestationAlreadyRecorded + ); + }); +} - InvalidBlockReports::<Test>::insert(validator, true); +#[test] +fn register_ml_dsa_key_rejects_invalid_key() { + run_test(|| { + // Wrong length for ML-DSA-87. + let too_short: BoundedVec<u8, frame_support::traits::ConstU32<2592>> = + vec![0u8; 100].try_into().unwrap(); + assert_err!( + GhostConsensus::register_ml_dsa_key( + RuntimeOrigin::signed(1), + PqAlgorithm::MlDsa87, + too_short + ), + Error::<Test>::MlDsaKeyInvalid + ); + }); +} - let is_reported = InvalidBlockReports::<Test>::get(validator); - assert!(is_reported); - }); +#[test] +fn difficulty_retargets_toward_target_block_time() { + run_test(|| { + let start = Difficulty::<Test>::get(); + + // Establish the retarget baseline at block 1, t = 1000ms. + System::set_block_number(1); + Timestamp::set_timestamp(1_000); + GhostConsensus::adjust_difficulty(); + assert_eq!(LastRetargetBlock::<Test>::get(), 1); + + // 20 blocks but only 20_000ms elapsed => 1000ms/block, far faster than the + // 5000ms target => difficulty must rise (clamped to 4x). + System::set_block_number(21); + Timestamp::set_timestamp(21_000); + GhostConsensus::adjust_difficulty(); + let faster = Difficulty::<Test>::get(); + assert!(faster > start, "fast blocks should raise difficulty"); + + // 20 blocks with 400_000ms elapsed => 20_000ms/block, far slower than target + // => difficulty must fall. + System::set_block_number(41); + Timestamp::set_timestamp(421_000); + GhostConsensus::adjust_difficulty(); + let slower = Difficulty::<Test>::get(); + assert!(slower < faster, "slow blocks should lower difficulty"); + }); } diff --git a/pallets/pallet-ghost-consensus/src/types.rs b/pallets/pallet-ghost-consensus/src/types.rs index 3aa6c21..6c3c0d7 100644 --- a/pallets/pallet-ghost-consensus/src/types.rs +++ b/pallets/pallet-ghost-consensus/src/types.rs @@ -1,133 +1,132 @@ -//! Types for the Ghost Consensus Pallet +//! Types for the Ghost consensus pallet. use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{traits::ConstU32, BoundedVec}; use scale_info::TypeInfo; use sp_core::H256; -/// Block header for Ghost consensus -#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] +/// Block header stored by the Ghost consensus pallet. +#[derive(Clone, Debug, Default, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub struct GhostBlockHeader { - /// Block number - pub number: u32, - /// Parent hash - pub parent_hash: H256, - /// State root - pub state_root: H256, - /// Extrinsics root - pub extrinsics_root: H256, - /// PoW nonce - pub nonce: u64, - /// PoW difficulty - pub difficulty: u64, - /// PoS validator signature - pub validator_signature: Option<H256>, - /// PQC finalization signature - pub pqc_signature: Option<PqcSignature>, + pub number: u32, + pub parent_hash: H256, + pub state_root: H256, + pub extrinsics_root: H256, + pub nonce: u64, + pub difficulty: u64, + pub validator_signature: Option<H256>, } -/// PQC Signature for Crystal-Dilithium +/// Stake tracked for a validator candidate. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub struct PqcSignature(pub [u8; 4595]); - -impl Default for PqcSignature { - fn default() -> Self { - Self([0u8; 4595]) - } +pub struct ValidatorStake<AccountId, Balance> { + pub account: AccountId, + pub stake: Balance, + pub weight: u64, } -/// Mining difficulty adjustment +/// Result of weighted validator selection. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub struct DifficultyAdjustment { - /// Current difficulty - pub current: u64, - /// Target block time in milliseconds - pub target_block_time: u64, - /// Adjustment factor - pub adjustment_factor: u64, +pub struct PosSelection<AccountId> { + pub validator: AccountId, + pub weight: u64, + pub round: u64, } -/// Validator stake information +/// Block reward split between the miner and stakers. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub struct ValidatorStake<AccountId, Balance> { - /// Validator account - pub account: AccountId, - /// Staked amount - pub stake: Balance, - /// Stake weight for selection - pub weight: u64, +pub struct BlockReward<Balance> { + pub total: Balance, + pub miner_reward: Balance, + pub stakers_reward: Balance, } -/// PoW mining result -#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub struct PowResult { - /// Nonce found - pub nonce: u64, - /// Hash of the block - pub hash: H256, - /// Mining difficulty met - pub difficulty: u64, +/// Current phase of the Ghost pallet state machine. +#[derive(Clone, Debug, Default, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub enum ConsensusPhase { + #[default] + PowMining, + PosValidation, + Finalization, } -/// PoS validator selection result +/// Why a validator was slashed. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub struct PosSelection<AccountId> { - /// Selected validator - pub validator: AccountId, - /// Selection weight - pub weight: u64, - /// Selection round - pub round: u64, +pub enum SlashingReason { + DoubleSigning, + InvalidBlock, + Downtime, + Other, } -/// Block reward distribution +/// Evidence supplied with a slashing report. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub struct BlockReward<Balance> { - /// Total block reward - pub total: Balance, - /// Miner reward (40%) - pub miner_reward: Balance, - /// Stakers reward (60%) - pub stakers_reward: Balance, +pub enum MisbehaviorEvidence { + DoubleSigning { first_vote: H256, second_vote: H256 }, + InvalidBlock { block_number: u32 }, + Downtime, + Other { proof_hash: H256 }, } -/// Consensus phase -#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum ConsensusPhase { - /// Proof-of-Work mining phase - PowMining, - /// Proof-of-Stake validation phase - PosValidation, - /// Block finalization phase - Finalization, +/// Claimed post-quantum primitive family recorded in metadata. +#[derive(Clone, Debug, Default, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub enum PqAlgorithm { + #[default] + Unknown, + MlDsa44, + MlDsa65, + MlDsa87, + Falcon512, + Falcon1024, + SphincsPlus, + Hybrid, + Other([u8; 16]), } - -impl Default for ConsensusPhase { - fn default() -> Self { - Self::PowMining - } +/// Opaque proof artifact category tracked by claimed PQ metadata. +#[derive(Clone, Debug, Default, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub enum PqProofKind { + #[default] + Unknown, + Signature, + AggregateSignature, + KeyEncapsulation, + Attestation, + Transcript, + Other, } -/// Block validation status +/// Versioned metadata describing a claimed PQ key or proof bundle. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum BlockValidationStatus { - /// Block is valid - Valid, - /// Block is invalid - Invalid, - /// Block validation pending - Pending, +pub struct PqReadinessMetadata<BlockNumber> { + pub version: u16, + pub algorithm: PqAlgorithm, + pub proof_kind: PqProofKind, + pub key_strength_bits: u16, + pub claimed_nist_level: Option<u8>, + pub issued_at: Option<BlockNumber>, + pub expires_at: Option<BlockNumber>, + pub public_key_commitment: H256, + pub metadata_hash: Option<H256>, + pub flags: u8, } -/// Slashing reason +/// Bounded opaque PQ proof payload plus hashes for off-chain consistency checks. #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum SlashingReason { - /// Double signing detected - DoubleSigning, - /// Invalid block produced - InvalidBlock, - /// Validator offline - Downtime, - /// Other slashing reason - Other, +pub struct PqProofEnvelope<BlockNumber, const MAX_PROOF_BYTES: u32, const MAX_CONTEXT_BYTES: u32> { + pub algorithm: PqAlgorithm, + pub proof_kind: PqProofKind, + pub submitted_at: BlockNumber, + pub statement_hash: H256, + pub public_key_commitment: H256, + pub proof: BoundedVec<u8, ConstU32<MAX_PROOF_BYTES>>, + pub context: BoundedVec<u8, ConstU32<MAX_CONTEXT_BYTES>>, + pub auxiliary_hash: Option<H256>, } + +/// Default bounded opaque proof type for pallet storage and extrinsic payloads. +pub type DefaultPqProof<BlockNumber> = PqProofEnvelope<BlockNumber, 4096, 256>; + +/// Backward-compatible alias used by test/genesis helpers. +pub type GenesisHeaderInit = (u32, H256, H256, H256, u64, u64, Option<H256>); diff --git a/pallets/pallet-ghost-consensus/src/weights.rs b/pallets/pallet-ghost-consensus/src/weights.rs new file mode 100644 index 0000000..1599b04 --- /dev/null +++ b/pallets/pallet-ghost-consensus/src/weights.rs @@ -0,0 +1,111 @@ +//! Weights for `pallet-ghost-consensus`. +//! +//! These are conservative, operation-grounded weights rather than machine-generated +//! benchmark numbers. Each dispatchable's weight is derived from the storage reads/writes +//! it actually performs (via `T::DbWeight`, i.e. `RocksDbWeight` in the runtime) plus a +//! generous fixed compute allowance. In particular, dispatchables that run on-chain +//! ML-DSA-87 (FIPS 204) verification (`validate_block` when the selected validator has a +//! registered key, and `verify_pq_signature`) include a ~300µs `ML_DSA_VERIFY` allowance. +//! +//! They deliberately OVER-estimate so block space cannot be exhausted by under-priced +//! calls. The `benchmarking` module (built under `--features runtime-benchmarks`) can be +//! used with `frame-benchmarking` to replace these with empirical, machine-specific +//! weights before mainnet; this impl provides the complete `WeightInfo` trait in the +//! meantime and is what the runtime binds. + +use crate::{Config, WeightInfo}; +use core::marker::PhantomData; +use frame_support::{traits::Get, weights::Weight}; + +/// Conservative allowance for one on-chain ML-DSA-87 (FIPS 204) signature verification. +/// ML-DSA-87 verification costs on the order of a few hundred microseconds; 300µs +/// (300_000_000 ps of ref-time) is a deliberate over-estimate. +const ML_DSA_VERIFY: Weight = Weight::from_parts(300_000_000, 0); + +/// Weight functions for `pallet-ghost-consensus`, parameterised by the runtime `T` so the +/// real `DbWeight` and `MaxValidators` are used. +pub struct SubstrateWeight<T>(PhantomData<T>); + +impl<T: Config> WeightInfo for SubstrateWeight<T> { + /// Phase check + parent/difficulty reads + stake-weighted selection over up to + /// `MaxValidators` stakers, then either entering PoS validation or finalizing + /// immediately. Reads scale with the staker set; writes are bounded. + fn submit_block() -> Weight { + let max_validators = T::MaxValidators::get() as u64; + T::DbWeight::get() + .reads_writes(4u64.saturating_add(max_validators), 8) + .saturating_add(Weight::from_parts(20_000_000, 0)) + } + + /// Balance transfer into the pallet account plus stake/count bookkeeping. + fn stake() -> Weight { + T::DbWeight::get() + .reads_writes(6, 6) + .saturating_add(Weight::from_parts(15_000_000, 0)) + } + + /// Balance transfer back to the staker plus stake/count bookkeeping. + fn unstake() -> Weight { + T::DbWeight::get() + .reads_writes(6, 6) + .saturating_add(Weight::from_parts(15_000_000, 0)) + } + + /// Worst case: the selected validator has a registered ML-DSA key, so this runs a + /// real on-chain ML-DSA-87 verification, then distributes rewards to the miner and up + /// to `MaxValidators` stakers. + fn validate_block() -> Weight { + let max_validators = T::MaxValidators::get() as u64; + T::DbWeight::get() + .reads_writes( + 8u64.saturating_add(max_validators), + 8u64.saturating_add(max_validators), + ) + .saturating_add(ML_DSA_VERIFY) + .saturating_add(Weight::from_parts(20_000_000, 0)) + } + + /// Evidence validation plus a slashing-record push and stake reduction. + fn report_misbehavior() -> Weight { + T::DbWeight::get() + .reads_writes(6, 6) + .saturating_add(Weight::from_parts(20_000_000, 0)) + } + + fn register_pq_readiness(metadata_len: u32) -> Weight { + T::DbWeight::get() + .reads_writes(3, 3) + .saturating_add(Weight::from_parts(10_000_000, 0)) + .saturating_add(Weight::from_parts(2_000, 0).saturating_mul(metadata_len.into())) + } + + fn attest_pq_readiness(proof_len: u32) -> Weight { + T::DbWeight::get() + .reads_writes(4, 4) + .saturating_add(Weight::from_parts(15_000_000, 0)) + .saturating_add(Weight::from_parts(2_000, 0).saturating_mul(proof_len.into())) + } + + fn remove_pq_readiness() -> Weight { + T::DbWeight::get() + .reads_writes(3, 3) + .saturating_add(Weight::from_parts(10_000_000, 0)) + } + + /// Validates the ML-DSA public-key length/algorithm, then stores key + algorithm. + fn register_ml_dsa_key(key_len: u32) -> Weight { + T::DbWeight::get() + .reads_writes(2, 2) + .saturating_add(Weight::from_parts(15_000_000, 0)) + .saturating_add(Weight::from_parts(300, 0).saturating_mul(key_len.into())) + } + + /// Dominated by the on-chain ML-DSA-87 verification; also reads the registered key and + /// records the attestation (with the replay-guard existence check). + fn verify_pq_signature(sig_len: u32) -> Weight { + T::DbWeight::get() + .reads_writes(3, 2) + .saturating_add(ML_DSA_VERIFY) + .saturating_add(Weight::from_parts(300, 0).saturating_mul(sig_len.into())) + } +} diff --git a/pallets/template/src/lib.rs b/pallets/template/src/lib.rs index db01d16..a9a5ad2 100644 --- a/pallets/template/src/lib.rs +++ b/pallets/template/src/lib.rs @@ -63,141 +63,141 @@ pub use weights::*; // All pallet logic is defined in its own module and must be annotated by the `pallet` attribute. #[frame_support::pallet] pub mod pallet { - // Import various useful types required by all FRAME pallets. - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - // The `Pallet` struct serves as a placeholder to implement traits, methods and dispatchables - // (`Call`s) in this pallet. - #[pallet::pallet] - pub struct Pallet<T>(_); - - /// The pallet's configuration trait. - /// - /// All our types and constants a pallet depends on must be declared here. - /// These types are defined generically and made concrete when the pallet is declared in the - /// `runtime/src/lib.rs` file of your chain. - #[pallet::config] - pub trait Config: frame_system::Config { - /// The overarching runtime event type. - #[allow(deprecated)] - type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; - /// A type representing the weights required by the dispatchables of this pallet. - type WeightInfo: WeightInfo; - } - - /// A storage item for this pallet. - /// - /// In this template, we are declaring a storage item called `Something` that stores a single - /// `u32` value. Learn more about runtime storage here: <https://docs.substrate.io/build/runtime-storage/> - #[pallet::storage] - pub type Something<T> = StorageValue<_, u32>; - - /// Events that functions in this pallet can emit. - /// - /// Events are a simple means of indicating to the outside world (such as dApps, chain explorers - /// or other users) that some notable update in the runtime has occurred. In a FRAME pallet, the - /// documentation for each event field and its parameters is added to a node's metadata so it - /// can be used by external interfaces or tools. - /// - /// The `generate_deposit` macro generates a function on `Pallet` called `deposit_event` which - /// will convert the event type of your pallet into `RuntimeEvent` (declared in the pallet's - /// [`Config`] trait) and deposit it using [`frame_system::Pallet::deposit_event`]. - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event<T: Config> { - /// A user has successfully set a new value. - SomethingStored { - /// The new value set. - something: u32, - /// The account who set the new value. - who: T::AccountId, - }, - } - - /// Errors that can be returned by this pallet. - /// - /// Errors tell users that something went wrong so it's important that their naming is - /// informative. Similar to events, error documentation is added to a node's metadata so it's - /// equally important that they have helpful documentation associated with them. - /// - /// This type of runtime error can be up to 4 bytes in size should you want to return additional - /// information. - #[pallet::error] - pub enum Error<T> { - /// The value retrieved was `None` as no value was previously set. - NoneValue, - /// There was an attempt to increment the value in storage over `u32::MAX`. - StorageOverflow, - } - - /// The pallet's dispatchable functions ([`Call`]s). - /// - /// Dispatchable functions allows users to interact with the pallet and invoke state changes. - /// These functions materialize as "extrinsics", which are often compared to transactions. - /// They must always return a `DispatchResult` and be annotated with a weight and call index. - /// - /// The [`call_index`] macro is used to explicitly - /// define an index for calls in the [`Call`] enum. This is useful for pallets that may - /// introduce new dispatchables over time. If the order of a dispatchable changes, its index - /// will also change which will break backwards compatibility. - /// - /// The [`weight`] macro is used to assign a weight to each call. - #[pallet::call] - impl<T: Config> Pallet<T> { - /// An example dispatchable that takes a single u32 value as a parameter, writes the value - /// to storage and emits an event. - /// - /// It checks that the _origin_ for this call is _Signed_ and returns a dispatch - /// error if it isn't. Learn more about origins here: <https://docs.substrate.io/build/origins/> - #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::do_something())] - pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult { - // Check that the extrinsic was signed and get the signer. - let who = ensure_signed(origin)?; - - // Update storage. - Something::<T>::put(something); - - // Emit an event. - Self::deposit_event(Event::SomethingStored { something, who }); - - // Return a successful `DispatchResult` - Ok(()) - } - - /// An example dispatchable that may throw a custom error. - /// - /// It checks that the caller is a signed origin and reads the current value from the - /// `Something` storage item. If a current value exists, it is incremented by 1 and then - /// written back to storage. - /// - /// ## Errors - /// - /// The function will return an error under the following conditions: - /// - /// - If no value has been set ([`Error::NoneValue`]) - /// - If incrementing the value in storage causes an arithmetic overflow - /// ([`Error::StorageOverflow`]) - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::cause_error())] - pub fn cause_error(origin: OriginFor<T>) -> DispatchResult { - let _who = ensure_signed(origin)?; - - // Read a value from storage. - match Something::<T>::get() { - // Return an error if the value has not been set. - None => Err(Error::<T>::NoneValue.into()), - Some(old) => { - // Increment the value read from storage. This will cause an error in the event - // of overflow. - let new = old.checked_add(1).ok_or(Error::<T>::StorageOverflow)?; - // Update the value in storage with the incremented result. - Something::<T>::put(new); - Ok(()) - }, - } - } - } + // Import various useful types required by all FRAME pallets. + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + // The `Pallet` struct serves as a placeholder to implement traits, methods and dispatchables + // (`Call`s) in this pallet. + #[pallet::pallet] + pub struct Pallet<T>(_); + + /// The pallet's configuration trait. + /// + /// All our types and constants a pallet depends on must be declared here. + /// These types are defined generically and made concrete when the pallet is declared in the + /// `runtime/src/lib.rs` file of your chain. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching runtime event type. + #[allow(deprecated)] + type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; + /// A type representing the weights required by the dispatchables of this pallet. + type WeightInfo: WeightInfo; + } + + /// A storage item for this pallet. + /// + /// In this template, we are declaring a storage item called `Something` that stores a single + /// `u32` value. Learn more about runtime storage here: <https://docs.substrate.io/build/runtime-storage/> + #[pallet::storage] + pub type Something<T> = StorageValue<_, u32>; + + /// Events that functions in this pallet can emit. + /// + /// Events are a simple means of indicating to the outside world (such as dApps, chain explorers + /// or other users) that some notable update in the runtime has occurred. In a FRAME pallet, the + /// documentation for each event field and its parameters is added to a node's metadata so it + /// can be used by external interfaces or tools. + /// + /// The `generate_deposit` macro generates a function on `Pallet` called `deposit_event` which + /// will convert the event type of your pallet into `RuntimeEvent` (declared in the pallet's + /// [`Config`] trait) and deposit it using [`frame_system::Pallet::deposit_event`]. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + /// A user has successfully set a new value. + SomethingStored { + /// The new value set. + something: u32, + /// The account who set the new value. + who: T::AccountId, + }, + } + + /// Errors that can be returned by this pallet. + /// + /// Errors tell users that something went wrong so it's important that their naming is + /// informative. Similar to events, error documentation is added to a node's metadata so it's + /// equally important that they have helpful documentation associated with them. + /// + /// This type of runtime error can be up to 4 bytes in size should you want to return additional + /// information. + #[pallet::error] + pub enum Error<T> { + /// The value retrieved was `None` as no value was previously set. + NoneValue, + /// There was an attempt to increment the value in storage over `u32::MAX`. + StorageOverflow, + } + + /// The pallet's dispatchable functions ([`Call`]s). + /// + /// Dispatchable functions allows users to interact with the pallet and invoke state changes. + /// These functions materialize as "extrinsics", which are often compared to transactions. + /// They must always return a `DispatchResult` and be annotated with a weight and call index. + /// + /// The [`call_index`] macro is used to explicitly + /// define an index for calls in the [`Call`] enum. This is useful for pallets that may + /// introduce new dispatchables over time. If the order of a dispatchable changes, its index + /// will also change which will break backwards compatibility. + /// + /// The [`weight`] macro is used to assign a weight to each call. + #[pallet::call] + impl<T: Config> Pallet<T> { + /// An example dispatchable that takes a single u32 value as a parameter, writes the value + /// to storage and emits an event. + /// + /// It checks that the _origin_ for this call is _Signed_ and returns a dispatch + /// error if it isn't. Learn more about origins here: <https://docs.substrate.io/build/origins/> + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::do_something())] + pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult { + // Check that the extrinsic was signed and get the signer. + let who = ensure_signed(origin)?; + + // Update storage. + Something::<T>::put(something); + + // Emit an event. + Self::deposit_event(Event::SomethingStored { something, who }); + + // Return a successful `DispatchResult` + Ok(()) + } + + /// An example dispatchable that may throw a custom error. + /// + /// It checks that the caller is a signed origin and reads the current value from the + /// `Something` storage item. If a current value exists, it is incremented by 1 and then + /// written back to storage. + /// + /// ## Errors + /// + /// The function will return an error under the following conditions: + /// + /// - If no value has been set ([`Error::NoneValue`]) + /// - If incrementing the value in storage causes an arithmetic overflow + /// ([`Error::StorageOverflow`]) + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::cause_error())] + pub fn cause_error(origin: OriginFor<T>) -> DispatchResult { + let _who = ensure_signed(origin)?; + + // Read a value from storage. + match Something::<T>::get() { + // Return an error if the value has not been set. + None => Err(Error::<T>::NoneValue.into()), + Some(old) => { + // Increment the value read from storage. This will cause an error in the event + // of overflow. + let new = old.checked_add(1).ok_or(Error::<T>::StorageOverflow)?; + // Update the value in storage with the incremented result. + Something::<T>::put(new); + Ok(()) + } + } + } + } } diff --git a/pallets/template/src/mock.rs b/pallets/template/src/mock.rs index 44085bc..b4b421b 100644 --- a/pallets/template/src/mock.rs +++ b/pallets/template/src/mock.rs @@ -6,41 +6,43 @@ type Block = frame_system::mocking::MockBlock<Test>; #[frame_support::runtime] mod runtime { - // The main runtime - #[runtime::runtime] - // Runtime Types to be generated - #[runtime::derive( - RuntimeCall, - RuntimeEvent, - RuntimeError, - RuntimeOrigin, - RuntimeFreezeReason, - RuntimeHoldReason, - RuntimeSlashReason, - RuntimeLockId, - RuntimeTask, - RuntimeViewFunction - )] - pub struct Test; + // The main runtime + #[runtime::runtime] + // Runtime Types to be generated + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask + )] + pub struct Test; - #[runtime::pallet_index(0)] - pub type System = frame_system::Pallet<Test>; + #[runtime::pallet_index(0)] + pub type System = frame_system::Pallet<Test>; - #[runtime::pallet_index(1)] - pub type Template = pallet_template::Pallet<Test>; + #[runtime::pallet_index(1)] + pub type Template = pallet_template::Pallet<Test>; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { - type Block = Block; + type Block = Block; } impl pallet_template::Config for Test { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - frame_system::GenesisConfig::<Test>::default().build_storage().unwrap().into() + frame_system::GenesisConfig::<Test>::default() + .build_storage() + .unwrap() + .into() } diff --git a/pallets/template/src/tests.rs b/pallets/template/src/tests.rs index d05433c..3dcb7d5 100644 --- a/pallets/template/src/tests.rs +++ b/pallets/template/src/tests.rs @@ -3,22 +3,31 @@ use frame_support::{assert_noop, assert_ok}; #[test] fn it_works_for_default_value() { - new_test_ext().execute_with(|| { - // Go past genesis block so events get deposited - System::set_block_number(1); - // Dispatch a signed extrinsic. - assert_ok!(Template::do_something(RuntimeOrigin::signed(1), 42)); - // Read pallet storage and assert an expected result. - assert_eq!(Something::<Test>::get(), Some(42)); - // Assert that the correct event was deposited - System::assert_last_event(Event::SomethingStored { something: 42, who: 1 }.into()); - }); + new_test_ext().execute_with(|| { + // Go past genesis block so events get deposited + System::set_block_number(1); + // Dispatch a signed extrinsic. + assert_ok!(Template::do_something(RuntimeOrigin::signed(1), 42)); + // Read pallet storage and assert an expected result. + assert_eq!(Something::<Test>::get(), Some(42)); + // Assert that the correct event was deposited + System::assert_last_event( + Event::SomethingStored { + something: 42, + who: 1, + } + .into(), + ); + }); } #[test] fn correct_error_for_none_value() { - new_test_ext().execute_with(|| { - // Ensure the expected error is thrown when no value is present. - assert_noop!(Template::cause_error(RuntimeOrigin::signed(1)), Error::<Test>::NoneValue); - }); + new_test_ext().execute_with(|| { + // Ensure the expected error is thrown when no value is present. + assert_noop!( + Template::cause_error(RuntimeOrigin::signed(1)), + Error::<Test>::NoneValue + ); + }); } diff --git a/polkadot-sdk b/polkadot-sdk new file mode 160000 index 0000000..5314442 --- /dev/null +++ b/polkadot-sdk @@ -0,0 +1 @@ +Subproject commit 5314442a060f53391c5d8d1ece4937332dfa9fc9 diff --git a/pqc/pqc_logic.rs b/pqc/pqc_logic.rs deleted file mode 100644 index 0d427f5..0000000 --- a/pqc/pqc_logic.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Conceptual Rust module for PQC Signature Verification in Ghost Chain -// Using Crystal-Dilithium as the primary PQC candidate - -use sp_core::Hasher; -use sp_runtime::traits::Verify; - -/// Quantum-resistant signature type (wrapping Dilithium-5) -#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] -pub struct PqcSignature(pub [u8; 4595]); // Dilithium5 sig size - -impl Verify for PqcSignature { - type Signer = sp_core::ed25519::Public; // Multi-sig fallback placeholder - - fn verify<L: sp_runtime::traits::Lazy<[u8]>>(&self, mut msg: L, signer: &Self::Signer) -> bool { - // Implementation: Hook into a WASM-optimized Dilithium crate - // 1. Load public key from 'signer' - // 2. Validate 'self.0' against 'msg.get()' - // 3. Ghost specific: Fallback to high-entropy PoW if verification fails due to congestion - true - } -} - -pub fn upgrade_to_quantum_finality() { - println!("Ghost Chain: Hardening consensus with Dilithium-5 signatures."); -} diff --git a/quantum_consensus.md b/quantum_consensus.md deleted file mode 100644 index a432631..0000000 --- a/quantum_consensus.md +++ /dev/null @@ -1,18 +0,0 @@ -# Ghost Chain: Quantum-Resistant Hybrid Consensus - -## The "2" Strategy: Quantum Hardening -We are integrating Post-Quantum Cryptography (PQC) into the Substrate pallet to make Ghost Chain "Future-Proof". - -### ⚖️ Hybrid PQC-Classical Signature Scheme -- **Primary:** Crystals-Dilithium (NIST Round 3 Winner). Used for block finalization by the PoS committee. -- **Security:** Level 5 (equivalent to AES-256). -- **PoW synergy:** The PoW puzzle now includes a "PQC hash-check" where miners must solve a lattice-based challenge, making them partially resistant to future quantum-accelerated hashing (Grover's algorithm). - -### 🛠️ Substrate Implementation -- Added `pqc_logic.rs` to the workspace. -- The committee signature verification will move from Ed25519 to Dilithium in the `finalize_block` phase. - -## Current Progress -1. [x] Drafted PQC signature wrapper. -2. [ ] Integrate lattice-based difficulty adjustment into PoW logic. -3. [ ] Benchmarking PQC signature verification in WASM. diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 7d12bb5..9ebc5b3 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -40,6 +40,7 @@ sp-api = { workspace = true } sp-block-builder = { workspace = true } sp-consensus-aura = { features = ["serde"], workspace = true } sp-consensus-grandpa = { features = ["serde"], workspace = true } +sp-consensus-pow = { workspace = true } sp-core = { features = ["serde"], workspace = true } sp-genesis-builder = { workspace = true } sp-inherents = { workspace = true } @@ -60,10 +61,17 @@ frame-benchmarking = { optional = true, workspace = true } frame-system-benchmarking = { optional = true, workspace = true } # The pallet in this template. -pallet-template = { workspace = true } +pallet-template = { workspace = true, default-features = false } # Ghost Consensus pallet -pallet-ghost-consensus = { path = "../pallets/pallet-ghost-consensus", default-features = false, features = ["std"] } +pallet-ghost-consensus = { path = "../pallets/pallet-ghost-consensus", default-features = false } + +# Several transitive crypto dependencies link `getrandom` even in the deterministic +# Wasm runtime, where no OS entropy source exists. Provide the `custom` backend so the +# runtime compiles for wasm32-unknown-unknown; the backend (registered in lib.rs) errors +# if ever called, which never happens during verification-only runtime execution. +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", default-features = false, features = ["custom"] } [build-dependencies] substrate-wasm-builder = { optional = true, workspace = true, default-features = true } @@ -84,6 +92,8 @@ std = [ "pallet-timestamp/std", "pallet-transaction-payment/std", "pallet-transaction-payment-rpc-runtime-api/std", + "frame-system-rpc-runtime-api/std", + "pallet-template/std", "pallet-ghost-consensus/std", "sp-core/std", "sp-runtime/std", @@ -91,6 +101,7 @@ std = [ "sp-block-builder/std", "sp-consensus-aura/std", "sp-consensus-grandpa/std", + "sp-consensus-pow/std", "sp-genesis-builder/std", "sp-inherents/std", "sp-keyring/std", diff --git a/runtime/src/apis.rs b/runtime/src/apis.rs index 34c498a..ea08b35 100644 --- a/runtime/src/apis.rs +++ b/runtime/src/apis.rs @@ -23,27 +23,23 @@ // // For more information, please refer to <http://unlicense.org> -// External crates imports use alloc::vec::Vec; use frame_support::{ genesis_builder_helper::{build_state, get_preset}, weights::Weight, }; -use pallet_grandpa::AuthorityId as GrandpaId; use sp_api::impl_runtime_apis; -use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; +use sp_core::{crypto::KeyTypeId, OpaqueMetadata, U256}; use sp_runtime::{ - traits::{Block as BlockT, NumberFor}, + traits::Block as BlockT, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, }; use sp_version::RuntimeVersion; -// Local module imports use super::{ - AccountId, Aura, Balance, Block, Executive, Grandpa, InherentDataExt, Nonce, Runtime, - RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, VERSION, + AccountId, Balance, Block, Executive, InherentDataExt, Nonce, Runtime, RuntimeCall, + RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, VERSION, }; impl_runtime_apis! { @@ -75,12 +71,6 @@ impl_runtime_apis! { } } - impl frame_support::view_functions::runtime_api::RuntimeViewFunction<Block> for Runtime { - fn execute_view_function(id: frame_support::view_functions::ViewFunctionId, input: Vec<u8>) -> Result<Vec<u8>, frame_support::view_functions::ViewFunctionDispatchError> { - Runtime::execute_view_function(id, input) - } - } - impl sp_block_builder::BlockBuilder<Block> for Runtime { fn apply_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> ApplyExtrinsicResult { Executive::apply_extrinsic(extrinsic) @@ -118,13 +108,11 @@ impl_runtime_apis! { } } - impl sp_consensus_aura::AuraApi<Block, AuraId> for Runtime { - fn slot_duration() -> sp_consensus_aura::SlotDuration { - sp_consensus_aura::SlotDuration::from_millis(Aura::slot_duration()) - } - - fn authorities() -> Vec<AuraId> { - pallet_aura::Authorities::<Runtime>::get().into_inner() + impl sp_consensus_pow::DifficultyApi<Block, U256> for Runtime { + fn difficulty() -> U256 { + // PoW difficulty (conventional: higher = harder) maintained on-chain by + // pallet-ghost-consensus and retargeted toward the target block time. + U256::from(pallet_ghost_consensus::Difficulty::<Runtime>::get()) } } @@ -140,36 +128,6 @@ impl_runtime_apis! { } } - impl sp_consensus_grandpa::GrandpaApi<Block> for Runtime { - fn grandpa_authorities() -> sp_consensus_grandpa::AuthorityList { - Grandpa::grandpa_authorities() - } - - fn current_set_id() -> sp_consensus_grandpa::SetId { - Grandpa::current_set_id() - } - - fn submit_report_equivocation_unsigned_extrinsic( - _equivocation_proof: sp_consensus_grandpa::EquivocationProof< - <Block as BlockT>::Hash, - NumberFor<Block>, - >, - _key_owner_proof: sp_consensus_grandpa::OpaqueKeyOwnershipProof, - ) -> Option<()> { - None - } - - fn generate_key_ownership_proof( - _set_id: sp_consensus_grandpa::SetId, - _authority_id: GrandpaId, - ) -> Option<sp_consensus_grandpa::OpaqueKeyOwnershipProof> { - // NOTE: this is the only implementation possible since we've - // defined our key owner proof type as a bottom type (i.e. a type - // with no values). - None - } - } - impl frame_system_rpc_runtime_api::AccountNonceApi<Block, AccountId, Nonce> for Runtime { fn account_nonce(account: AccountId) -> Nonce { System::account_nonce(account) @@ -226,11 +184,11 @@ impl_runtime_apis! { Vec<frame_benchmarking::BenchmarkList>, Vec<frame_support::traits::StorageInfo>, ) { + use baseline::Pallet as BaselineBench; use frame_benchmarking::{baseline, BenchmarkList}; use frame_support::traits::StorageInfoTrait; - use frame_system_benchmarking::Pallet as SystemBench; use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; - use baseline::Pallet as BaselineBench; + use frame_system_benchmarking::Pallet as SystemBench; use super::*; let mut list = Vec::<BenchmarkList>::new(); @@ -245,17 +203,17 @@ impl_runtime_apis! { fn dispatch_benchmark( config: frame_benchmarking::BenchmarkConfig ) -> Result<Vec<frame_benchmarking::BenchmarkBatch>, alloc::string::String> { + use baseline::Pallet as BaselineBench; use frame_benchmarking::{baseline, BenchmarkBatch}; - use sp_storage::TrackedStorageKey; - use frame_system_benchmarking::Pallet as SystemBench; + use frame_support::traits::WhitelistedStorageKeys; use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; - use baseline::Pallet as BaselineBench; + use frame_system_benchmarking::Pallet as SystemBench; + use sp_storage::TrackedStorageKey; use super::*; impl frame_system_benchmarking::Config for Runtime {} impl baseline::Config for Runtime {} - use frame_support::traits::WhitelistedStorageKeys; let whitelist: Vec<TrackedStorageKey> = AllPalletsWithSystem::whitelisted_storage_keys(); let mut batches = Vec::<BenchmarkBatch>::new(); @@ -269,9 +227,6 @@ impl_runtime_apis! { #[cfg(feature = "try-runtime")] impl frame_try_runtime::TryRuntime<Block> for Runtime { fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) { - // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to - // have a backtrace here. If any of the pre/post migration checks fail, we shall stop - // right here and right now. let weight = Executive::try_runtime_upgrade(checks).unwrap(); (weight, super::configs::RuntimeBlockWeights::get().max_block) } @@ -282,9 +237,8 @@ impl_runtime_apis! { signature_check: bool, select: frame_try_runtime::TryStateSelect ) -> Weight { - // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to - // have a backtrace here. - Executive::try_execute_block(block, state_root_check, signature_check, select).expect("execute-block failed") + Executive::try_execute_block(block, state_root_check, signature_check, select) + .expect("execute-block failed") } } @@ -301,5 +255,4 @@ impl_runtime_apis! { crate::genesis_config_presets::preset_names() } } - } diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index 59012e0..245ac58 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -24,11 +24,12 @@ // For more information, please refer to <http://unlicense.org> frame_benchmarking::define_benchmarks!( - [frame_benchmarking, BaselineBench::<Runtime>] - [frame_system, SystemBench::<Runtime>] - [frame_system_extensions, SystemExtensionsBench::<Runtime>] - [pallet_balances, Balances] - [pallet_timestamp, Timestamp] - [pallet_sudo, Sudo] - [pallet_template, Template] + [frame_benchmarking, BaselineBench::<Runtime>] + [frame_system, SystemBench::<Runtime>] + [frame_system_extensions, SystemExtensionsBench::<Runtime>] + [pallet_balances, Balances] + [pallet_ghost_consensus, GhostConsensus] + [pallet_timestamp, Timestamp] + [pallet_sudo, Sudo] + [pallet_template, Template] ); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 1960f91..fe15909 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -25,39 +25,38 @@ // Substrate and Polkadot dependencies use frame_support::{ - derive_impl, parameter_types, - traits::{ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, VariantCountOf}, - weights::{ - constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, - IdentityFee, Weight, - }, + derive_impl, parameter_types, + traits::{ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, VariantCountOf}, + weights::{ + constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, + IdentityFee, Weight, + }, }; use frame_system::limits::{BlockLength, BlockWeights}; use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier}; -use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_runtime::{traits::One, Perbill}; use sp_version::RuntimeVersion; // Local module imports use super::{ - AccountId, Aura, Balance, Balances, Block, BlockNumber, Hash, Nonce, PalletInfo, Runtime, - RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, - System, EXISTENTIAL_DEPOSIT, SLOT_DURATION, VERSION, + AccountId, Balance, Balances, Block, BlockNumber, Hash, Nonce, PalletInfo, Runtime, + RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, + System, EXISTENTIAL_DEPOSIT, SLOT_DURATION, UNIT, VERSION, }; const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); parameter_types! { - pub const BlockHashCount: BlockNumber = 2400; - pub const Version: RuntimeVersion = VERSION; - - /// We allow for 2 seconds of compute with a 6 second average block time. - pub RuntimeBlockWeights: BlockWeights = BlockWeights::with_sensible_defaults( - Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX), - NORMAL_DISPATCH_RATIO, - ); - pub RuntimeBlockLength: BlockLength = BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); - pub const SS58Prefix: u8 = 42; + pub const BlockHashCount: BlockNumber = 2400; + pub const Version: RuntimeVersion = VERSION; + + /// We allow for 2 seconds of compute with a 6 second average block time. + pub RuntimeBlockWeights: BlockWeights = BlockWeights::with_sensible_defaults( + Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX), + NORMAL_DISPATCH_RATIO, + ); + pub RuntimeBlockLength: BlockLength = BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); + pub const SS58Prefix: u8 = 42; } /// All migrations of the runtime, aside from the ones declared in the pallets. @@ -71,114 +70,111 @@ type SingleBlockMigrations = (); /// but overridden as needed. #[derive_impl(frame_system::config_preludes::SolochainDefaultConfig)] impl frame_system::Config for Runtime { - /// The block type for the runtime. - type Block = Block; - /// Block & extrinsics weights: base values and limits. - type BlockWeights = RuntimeBlockWeights; - /// The maximum length of a block (in bytes). - type BlockLength = RuntimeBlockLength; - /// The identifier used to distinguish between accounts. - type AccountId = AccountId; - /// The type for storing how many extrinsics an account has signed. - type Nonce = Nonce; - /// The type for hashing blocks and tries. - type Hash = Hash; - /// Maximum number of block number to block hash mappings to keep (oldest pruned first). - type BlockHashCount = BlockHashCount; - /// The weight of database operations that the runtime can invoke. - type DbWeight = RocksDbWeight; - /// Version of the runtime. - type Version = Version; - /// The data to be stored in an account. - type AccountData = pallet_balances::AccountData<Balance>; - /// This is used as an identifier of the chain. 42 is the generic substrate prefix. - type SS58Prefix = SS58Prefix; - type MaxConsumers = frame_support::traits::ConstU32<16>; - type SingleBlockMigrations = SingleBlockMigrations; -} - -impl pallet_aura::Config for Runtime { - type AuthorityId = AuraId; - type DisabledValidators = (); - type MaxAuthorities = ConstU32<32>; - type AllowMultipleBlocksPerSlot = ConstBool<false>; - type SlotDuration = pallet_aura::MinimumPeriodTimesTwo<Runtime>; -} - -impl pallet_grandpa::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - - type WeightInfo = (); - type MaxAuthorities = ConstU32<32>; - type MaxNominators = ConstU32<0>; - type MaxSetIdSessionEntries = ConstU64<0>; - - type KeyOwnerProof = sp_core::Void; - type EquivocationReportSystem = (); + /// The block type for the runtime. + type Block = Block; + /// Block & extrinsics weights: base values and limits. + type BlockWeights = RuntimeBlockWeights; + /// The maximum length of a block (in bytes). + type BlockLength = RuntimeBlockLength; + /// The identifier used to distinguish between accounts. + type AccountId = AccountId; + /// The type for storing how many extrinsics an account has signed. + type Nonce = Nonce; + /// The type for hashing blocks and tries. + type Hash = Hash; + /// Maximum number of block number to block hash mappings to keep (oldest pruned first). + type BlockHashCount = BlockHashCount; + /// The weight of database operations that the runtime can invoke. + type DbWeight = RocksDbWeight; + /// Version of the runtime. + type Version = Version; + /// The data to be stored in an account. + type AccountData = pallet_balances::AccountData<Balance>; + /// This is used as an identifier of the chain. 42 is the generic substrate prefix. + type SS58Prefix = SS58Prefix; + type MaxConsumers = frame_support::traits::ConstU32<16>; + type SingleBlockMigrations = SingleBlockMigrations; } impl pallet_timestamp::Config for Runtime { - /// A timestamp: milliseconds since the unix epoch. - type Moment = u64; - type OnTimestampSet = Aura; - type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; - type WeightInfo = (); + /// A timestamp: milliseconds since the unix epoch. + type Moment = u64; + // PoW has no slot cadence and Aura has been removed, so there is no + // on-timestamp-set hook. Block intervals are governed by PoW difficulty. + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; + type WeightInfo = (); } impl pallet_balances::Config for Runtime { - type MaxLocks = ConstU32<50>; - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; - /// The type for recording an account's balance. - type Balance = Balance; - /// The ubiquitous event type. - type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); - type ExistentialDeposit = ConstU128<EXISTENTIAL_DEPOSIT>; - type AccountStore = System; - type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>; - type FreezeIdentifier = RuntimeFreezeReason; - type MaxFreezes = VariantCountOf<RuntimeFreezeReason>; - type RuntimeHoldReason = RuntimeHoldReason; - type RuntimeFreezeReason = RuntimeFreezeReason; - type DoneSlashHandler = (); + type MaxLocks = ConstU32<50>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + /// The type for recording an account's balance. + type Balance = Balance; + /// The ubiquitous event type. + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<EXISTENTIAL_DEPOSIT>; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>; + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = VariantCountOf<RuntimeFreezeReason>; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; } parameter_types! { - pub FeeMultiplier: Multiplier = Multiplier::one(); + pub FeeMultiplier: Multiplier = Multiplier::one(); + pub const GhostBlockReward: Balance = 10 * UNIT; + pub const GhostTargetBlockTime: u64 = 5_000; + pub const GhostDifficultyAdjustmentPeriod: u32 = 20; + pub const GhostMinStake: Balance = UNIT; + pub const GhostMaxDowntimeBlocks: u32 = 100; + pub const GhostMaxValidationBlocks: u32 = 20; + pub const GhostMaxValidators: u32 = 64; + pub const GhostMaxSlashingRecords: u32 = 1024; + pub const GhostDoubleSignSlashPercentage: u8 = 50; + pub const GhostInvalidBlockSlashPercentage: u8 = 25; + pub const GhostDowntimeSlashPercentage: u8 = 10; + pub const GhostPalletId: frame_support::PalletId = frame_support::PalletId(*b"py/ghost"); } impl pallet_transaction_payment::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter<Balances, ()>; - type OperationalFeeMultiplier = ConstU8<5>; - type WeightToFee = IdentityFee<Balance>; - type LengthToFee = IdentityFee<Balance>; - type FeeMultiplierUpdate = ConstFeeMultiplier<FeeMultiplier>; - type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight<Runtime>; + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = FungibleAdapter<Balances, ()>; + type OperationalFeeMultiplier = ConstU8<5>; + type WeightToFee = IdentityFee<Balance>; + type LengthToFee = IdentityFee<Balance>; + type FeeMultiplierUpdate = ConstFeeMultiplier<FeeMultiplier>; } impl pallet_sudo::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type RuntimeCall = RuntimeCall; - type WeightInfo = pallet_sudo::weights::SubstrateWeight<Runtime>; + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type WeightInfo = pallet_sudo::weights::SubstrateWeight<Runtime>; } /// Configure the pallet-template in pallets/template. impl pallet_template::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_template::weights::SubstrateWeight<Runtime>; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_template::weights::SubstrateWeight<Runtime>; } /// Configure the Ghost Consensus pallet impl pallet_ghost_consensus::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_ghost_consensus::weights::SubstrateWeight<Runtime>; - type BlockReward = ConstU128<10_000_000_000_000>; // 10 Ghost tokens per block - type MinStake = ConstU128<1_000_000_000_000>; // 1 Ghost token minimum stake - type MaxDowntimeBlocks = ConstU32<100>; // Max 100 blocks of downtime - type DoubleSignSlashPercentage = ConstU8<50>; // 50% slash for double signing - type InvalidBlockSlashPercentage = ConstU8<25>; // 25% slash for invalid blocks - type DowntimeSlashPercentage = ConstU8<10>; // 10% slash for downtime - type PalletId = frame_support::PalletId(*b"py/ghost"); + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_ghost_consensus::weights::SubstrateWeight<Runtime>; + type TargetBlockTime = GhostTargetBlockTime; + type DifficultyAdjustmentPeriod = GhostDifficultyAdjustmentPeriod; + type BlockReward = GhostBlockReward; + type MinStake = GhostMinStake; + type MaxDowntimeBlocks = GhostMaxDowntimeBlocks; + type MaxValidationBlocks = GhostMaxValidationBlocks; + type MaxValidators = GhostMaxValidators; + type MaxSlashingRecords = GhostMaxSlashingRecords; + type DoubleSignSlashPercentage = GhostDoubleSignSlashPercentage; + type InvalidBlockSlashPercentage = GhostInvalidBlockSlashPercentage; + type DowntimeSlashPercentage = GhostDowntimeSlashPercentage; + type PalletId = GhostPalletId; } diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 6af8dc9..7142afb 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -1,109 +1,121 @@ -// This file is part of Substrate. +// This is free and unencumbered software released into the public domain. +// See <http://unlicense.org> -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig}; +use crate::{ + AccountId, Balance, BalancesConfig, GhostConsensusConfig, RuntimeGenesisConfig, SudoConfig, + UNIT, +}; use alloc::{vec, vec::Vec}; -use frame_support::build_struct_json_patch; +use pallet_ghost_consensus::types::GenesisHeaderInit; use serde_json::Value; -use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use sp_consensus_grandpa::AuthorityId as GrandpaId; -use sp_genesis_builder::{self, PresetId}; +use sp_core::H256; +use sp_genesis_builder::PresetId; use sp_keyring::Sr25519Keyring; +use sp_runtime::traits::{BlakeTwo256, Hash}; + +const ENDOWMENT: Balance = 1u128 << 60; +const INITIAL_GHOST_STAKE: Balance = 2 * UNIT; + +fn ghost_genesis_header() -> GenesisHeaderInit { + ( + 0, + H256::zero(), + BlakeTwo256::hash_of(&(0u32, "ghost-genesis-state")), + BlakeTwo256::hash_of(&(0u32, "ghost-genesis-extrinsics")), + 0, + 1_000_000_000_000, + None, + ) +} -// Returns the genesis config presets populated with given parameters. +/// Build a genesis config preset. PoW authoring needs no consensus authorities, so only +/// balances, sudo, and the Ghost consensus (PoS) staking state are seeded. fn testnet_genesis( - initial_authorities: Vec<(AuraId, GrandpaId)>, - endowed_accounts: Vec<AccountId>, - root: AccountId, + endowed_accounts: Vec<AccountId>, + root: AccountId, + ghost_validators: Vec<AccountId>, ) -> Value { - build_struct_json_patch!(RuntimeGenesisConfig { - balances: BalancesConfig { - balances: endowed_accounts - .iter() - .cloned() - .map(|k| (k, 1u128 << 60)) - .collect::<Vec<_>>(), - }, - aura: pallet_aura::GenesisConfig { - authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect::<Vec<_>>(), - }, - grandpa: pallet_grandpa::GenesisConfig { - authorities: initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect::<Vec<_>>(), - }, - sudo: SudoConfig { key: Some(root) }, - }) + let ghost_validator_stakes = ghost_validators + .into_iter() + .map(|account| (account, INITIAL_GHOST_STAKE)) + .collect::<Vec<_>>(); + + // stable2407 has no `build_struct_json_patch!`; build the full genesis config and + // serialize it. Unset pallet configs fall back to their defaults. + let config = RuntimeGenesisConfig { + balances: BalancesConfig { + balances: endowed_accounts + .iter() + .cloned() + .map(|k| (k, ENDOWMENT)) + .collect::<Vec<_>>(), + }, + sudo: SudoConfig { key: Some(root) }, + ghost_consensus: GhostConsensusConfig { + genesis_header: Some(ghost_genesis_header()), + validator_stakes: ghost_validator_stakes, + }, + ..Default::default() + }; + + serde_json::to_value(&config).expect("Genesis config must be serializable to JSON. qed.") } /// Return the development genesis config. pub fn development_config_genesis() -> Value { - testnet_genesis( - vec![( - sp_keyring::Sr25519Keyring::Alice.public().into(), - sp_keyring::Ed25519Keyring::Alice.public().into(), - )], - vec![ - Sr25519Keyring::Alice.to_account_id(), - Sr25519Keyring::Bob.to_account_id(), - Sr25519Keyring::AliceStash.to_account_id(), - Sr25519Keyring::BobStash.to_account_id(), - ], - sp_keyring::Sr25519Keyring::Alice.to_account_id(), - ) + let alice = Sr25519Keyring::Alice.to_account_id(); + let bob = Sr25519Keyring::Bob.to_account_id(); + + testnet_genesis( + vec![ + alice.clone(), + bob.clone(), + Sr25519Keyring::AliceStash.to_account_id(), + Sr25519Keyring::BobStash.to_account_id(), + ], + alice.clone(), + vec![alice, bob], + ) } /// Return the local genesis config preset. pub fn local_config_genesis() -> Value { - testnet_genesis( - vec![ - ( - sp_keyring::Sr25519Keyring::Alice.public().into(), - sp_keyring::Ed25519Keyring::Alice.public().into(), - ), - ( - sp_keyring::Sr25519Keyring::Bob.public().into(), - sp_keyring::Ed25519Keyring::Bob.public().into(), - ), - ], - Sr25519Keyring::iter() - .filter(|v| v != &Sr25519Keyring::One && v != &Sr25519Keyring::Two) - .map(|v| v.to_account_id()) - .collect::<Vec<_>>(), - Sr25519Keyring::Alice.to_account_id(), - ) + testnet_genesis( + Sr25519Keyring::iter() + .filter(|v| v != &Sr25519Keyring::One && v != &Sr25519Keyring::Two) + .map(|v| v.to_account_id()) + .collect::<Vec<_>>(), + Sr25519Keyring::Alice.to_account_id(), + vec![ + Sr25519Keyring::Alice.to_account_id(), + Sr25519Keyring::Bob.to_account_id(), + Sr25519Keyring::Charlie.to_account_id(), + ], + ) } +/// Preset identifiers. In stable2407 `PresetId` is a plain runtime string, so we define +/// the names locally instead of importing constants that do not exist in this SDK version. +pub const DEV_PRESET: &str = "development"; +pub const LOCAL_PRESET: &str = "local_testnet"; + /// Provides the JSON representation of predefined genesis config for given `id`. pub fn get_preset(id: &PresetId) -> Option<Vec<u8>> { - let patch = match id.as_ref() { - sp_genesis_builder::DEV_RUNTIME_PRESET => development_config_genesis(), - sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET => local_config_genesis(), - _ => return None, - }; - Some( - serde_json::to_string(&patch) - .expect("serialization to json is expected to work. qed.") - .into_bytes(), - ) + let patch = if id == &PresetId::from(DEV_PRESET) { + development_config_genesis() + } else if id == &PresetId::from(LOCAL_PRESET) { + local_config_genesis() + } else { + return None; + }; + Some( + serde_json::to_string(&patch) + .expect("serialization to json is expected to work. qed.") + .into_bytes(), + ) } /// List of supported presets. pub fn preset_names() -> Vec<PresetId> { - vec![ - PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET), - PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET), - ] + vec![PresetId::from(DEV_PRESET), PresetId::from(LOCAL_PRESET)] } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index de01ba0..e6be3a1 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -3,6 +3,18 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +// Custom `getrandom` backend for the Wasm runtime. Transitive crypto dependencies +// link `getrandom`, but a deterministic runtime never needs OS entropy. This backend +// errors if ever invoked (which does not happen during verification-only execution) +// and merely satisfies the linker for `wasm32-unknown-unknown`. +#[cfg(all(not(feature = "std"), target_arch = "wasm32"))] +mod wasm_getrandom_backend { + fn always_unsupported(_dest: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) + } + getrandom::register_custom_getrandom!(always_unsupported); +} + pub mod apis; #[cfg(feature = "runtime-benchmarks")] mod benchmarks; @@ -11,9 +23,9 @@ pub mod configs; extern crate alloc; use alloc::vec::Vec; use sp_runtime::{ - generic, impl_opaque_keys, - traits::{BlakeTwo256, IdentifyAccount, Verify}, - MultiAddress, MultiSignature, + create_runtime_str, generic, impl_opaque_keys, + traits::{BlakeTwo256, IdentifyAccount, Verify}, + MultiAddress, MultiSignature, }; #[cfg(feature = "std")] use sp_version::NativeVersion; @@ -32,62 +44,61 @@ pub mod genesis_config_presets; /// of data like extrinsics, allowing for them to continue syncing the network through upgrades /// to even the core data structures. pub mod opaque { - use super::*; - use sp_runtime::{ - generic, - traits::{BlakeTwo256, Hash as HashT}, - }; - - pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; - - /// Opaque block header type. - pub type Header = generic::Header<BlockNumber, BlakeTwo256>; - /// Opaque block type. - pub type Block = generic::Block<Header, UncheckedExtrinsic>; - /// Opaque block identifier type. - pub type BlockId = generic::BlockId<Block>; - /// Opaque block hash type. - pub type Hash = <BlakeTwo256 as HashT>::Output; + use super::*; + use sp_runtime::{ + generic, + traits::{BlakeTwo256, Hash as HashT}, + }; + + pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; + + /// Opaque block header type. + pub type Header = generic::Header<BlockNumber, BlakeTwo256>; + /// Opaque block type. + pub type Block = generic::Block<Header, UncheckedExtrinsic>; + /// Opaque block identifier type. + pub type BlockId = generic::BlockId<Block>; + /// Opaque block hash type. + pub type Hash = <BlakeTwo256 as HashT>::Output; } impl_opaque_keys! { - pub struct SessionKeys { - pub aura: Aura, - pub grandpa: Grandpa, - } + // PoW authoring uses no runtime session keys (Aura/GRANDPA removed). An empty + // key set keeps the SessionKeys runtime API and key tooling working. + pub struct SessionKeys {} } // To learn more about runtime versioning, see: // https://docs.substrate.io/main-docs/build/upgrade#runtime-versioning #[sp_version::runtime_version] pub const VERSION: RuntimeVersion = RuntimeVersion { - spec_name: alloc::borrow::Cow::Borrowed("ghost-runtime"), - impl_name: alloc::borrow::Cow::Borrowed("ghost-runtime"), - authoring_version: 1, - // The version of the runtime specification. A full node will not attempt to use its native - // runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, - // `spec_version`, and `authoring_version` are the same between Wasm and native. - // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use - // the compatible custom types. - spec_version: 100, - impl_version: 1, - apis: apis::RUNTIME_API_VERSIONS, - transaction_version: 1, - system_version: 1, + spec_name: create_runtime_str!("ghost-runtime"), + impl_name: create_runtime_str!("ghost-runtime"), + authoring_version: 1, + // The version of the runtime specification. A full node will not attempt to use its native + // runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, + // `spec_version`, and `authoring_version` are the same between Wasm and native. + // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use + // the compatible custom types. + spec_version: 100, + impl_version: 1, + apis: apis::RUNTIME_API_VERSIONS, + transaction_version: 1, + state_version: 1, }; mod block_times { - /// This determines the average expected block time that we are targeting. Blocks will be - /// produced at a minimum duration defined by `SLOT_DURATION`. `SLOT_DURATION` is picked up by - /// `pallet_timestamp` which is in turn picked up by `pallet_aura` to implement `fn - /// slot_duration()`. - /// - /// Change this to adjust the block time. - pub const MILLI_SECS_PER_BLOCK: u64 = 5000; // 5 seconds per block for Ghost - - // NOTE: Currently it is not possible to change the slot duration after the chain has started. - // Attempting to do so will brick block production. - pub const SLOT_DURATION: u64 = MILLI_SECS_PER_BLOCK; + /// This determines the average expected block time that we are targeting. Blocks will be + /// produced at a minimum duration defined by `SLOT_DURATION`. `SLOT_DURATION` is picked up by + /// `pallet_timestamp` which is in turn picked up by `pallet_aura` to implement `fn + /// slot_duration()`. + /// + /// Change this to adjust the block time. + pub const MILLI_SECS_PER_BLOCK: u64 = 5000; // 5 seconds per block for Ghost + + // NOTE: Currently it is not possible to change the slot duration after the chain has started. + // Attempting to do so will brick block production. + pub const SLOT_DURATION: u64 = MILLI_SECS_PER_BLOCK; } pub use block_times::*; @@ -109,7 +120,10 @@ pub const EXISTENTIAL_DEPOSIT: Balance = MILLI_UNIT; /// The version information used to identify this runtime when compiled natively. #[cfg(feature = "std")] pub fn native_version() -> NativeVersion { - NativeVersion { runtime_version: VERSION, can_author_with: Default::default() } + NativeVersion { + runtime_version: VERSION, + can_author_with: Default::default(), + } } /// Alias to 512-bit hash when used in the context of a transaction signature on the chain. @@ -148,79 +162,74 @@ pub type BlockId = generic::BlockId<Block>; /// The `TransactionExtension` to the basic transaction logic. pub type TxExtension = ( - frame_system::AuthorizeCall<Runtime>, - frame_system::CheckNonZeroSender<Runtime>, - frame_system::CheckSpecVersion<Runtime>, - frame_system::CheckTxVersion<Runtime>, - frame_system::CheckGenesis<Runtime>, - frame_system::CheckEra<Runtime>, - frame_system::CheckNonce<Runtime>, - frame_system::CheckWeight<Runtime>, - pallet_transaction_payment::ChargeTransactionPayment<Runtime>, - frame_metadata_hash_extension::CheckMetadataHash<Runtime>, - frame_system::WeightReclaim<Runtime>, + frame_system::CheckNonZeroSender<Runtime>, + frame_system::CheckSpecVersion<Runtime>, + frame_system::CheckTxVersion<Runtime>, + frame_system::CheckGenesis<Runtime>, + frame_system::CheckEra<Runtime>, + frame_system::CheckNonce<Runtime>, + frame_system::CheckWeight<Runtime>, + pallet_transaction_payment::ChargeTransactionPayment<Runtime>, + frame_metadata_hash_extension::CheckMetadataHash<Runtime>, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = - generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, TxExtension>; + generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, TxExtension>; /// The payload being signed in transactions. pub type SignedPayload = generic::SignedPayload<RuntimeCall, TxExtension>; /// Executive: handles dispatch to the various modules. pub type Executive = frame_executive::Executive< - Runtime, - Block, - frame_system::ChainContext<Runtime>, - Runtime, - AllPalletsWithSystem, + Runtime, + Block, + frame_system::ChainContext<Runtime>, + Runtime, + AllPalletsWithSystem, >; // Create the runtime by composing the FRAME pallets that were previously configured. #[frame_support::runtime] mod runtime { - #[runtime::runtime] - #[runtime::derive( - RuntimeCall, - RuntimeEvent, - RuntimeError, - RuntimeOrigin, - RuntimeFreezeReason, - RuntimeHoldReason, - RuntimeSlashReason, - RuntimeLockId, - RuntimeTask, - RuntimeViewFunction - )] - pub struct Runtime; - - #[runtime::pallet_index(0)] - pub type System = frame_system; - - #[runtime::pallet_index(1)] - pub type Timestamp = pallet_timestamp; - - #[runtime::pallet_index(2)] - pub type Aura = pallet_aura; - - #[runtime::pallet_index(3)] - pub type Grandpa = pallet_grandpa; - - #[runtime::pallet_index(4)] - pub type Balances = pallet_balances; - - #[runtime::pallet_index(5)] - pub type TransactionPayment = pallet_transaction_payment; - - #[runtime::pallet_index(6)] - pub type Sudo = pallet_sudo; - - // Include the custom logic from the pallet-template in the runtime. - #[runtime::pallet_index(7)] - pub type Template = pallet_template; - - // Include the Ghost Consensus pallet - #[runtime::pallet_index(8)] - pub type GhostConsensus = pallet_ghost_consensus; + #[runtime::runtime] + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask + )] + pub struct Runtime; + + #[runtime::pallet_index(0)] + pub type System = frame_system; + + #[runtime::pallet_index(1)] + pub type Timestamp = pallet_timestamp; + + // Indices 2 and 3 (previously Aura and GRANDPA) are intentionally left unused: + // block authoring is real Proof-of-Work (sc-consensus-pow), not slot-based Aura. + // Existing pallet indices are preserved to keep call/metadata encodings stable. + + #[runtime::pallet_index(4)] + pub type Balances = pallet_balances; + + #[runtime::pallet_index(5)] + pub type TransactionPayment = pallet_transaction_payment; + + #[runtime::pallet_index(6)] + pub type Sudo = pallet_sudo; + + // Include the custom logic from the pallet-template in the runtime. + #[runtime::pallet_index(7)] + pub type Template = pallet_template; + + // Include the Ghost Consensus pallet + #[runtime::pallet_index(8)] + pub type GhostConsensus = pallet_ghost_consensus; } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..b979d10 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable-x86_64-pc-windows-msvc" +profile = "default" +components = ["rustfmt", "clippy"] diff --git a/scripts/simulate_esc.py b/scripts/simulate_esc.py deleted file mode 100644 index 6506248..0000000 --- a/scripts/simulate_esc.py +++ /dev/null @@ -1,44 +0,0 @@ -import math -import collections - -def calculate_shannon_entropy(producers): - if not producers: return 0 - counts = collections.Counter(producers) - total = len(producers) - entropy = 0 - for count in counts.values(): - p_i = count / total - entropy -= p_i * math.log2(p_i) - return entropy - -def simulate_esc(): - # Healthy threshold from our pallet: 4.0 - threshold = 4.0 - current_difficulty = 1000000 - - print(f"--- ESC Simulation (Threshold: {threshold}) ---") - - # Scene 1: Decentralized (100 different producers) - producers_decentralized = [f"user_{i}" for i in range(100)] - ent_dec = calculate_shannon_entropy(producers_decentralized) - # adjustment = (5*100)/5 = 100 (no time change) - # penalty = (4.0 - 6.64) -> negative -> 0 - print(f"Decentralized Entropy: {ent_dec:.4f} | Difficulty: {current_difficulty}") - - # Scene 2: Centralized (1 miner taking 80% of blocks) - producers_centralized = ["whale"] * 80 + [f"user_{i}" for i in range(20)] - ent_cen = calculate_shannon_entropy(producers_centralized) - - # Manual check of our pallet logic: - # entropy_penalty = ((4,000,000 - 1,180,000) * 100) / 4,000,000 = ~70 - # adjustment_factor = 100 + 70 = 170 - # new_diff = (current_difficulty * 170) / 100 - - penalty_pct = max(0, (threshold - ent_cen) / threshold) * 100 - new_difficulty = current_difficulty * (100 + penalty_pct) / 100 - - print(f"Centralized Entropy: {ent_cen:.4f} | Penalty: +{penalty_pct:.1f}%") - print(f"Steered Difficulty: {int(new_difficulty)} (Harder to mine for the whale)") - -if __name__ == "__main__": - simulate_esc() diff --git a/scripts/test_pqc.rs b/scripts/test_pqc.rs deleted file mode 100644 index 23c1773..0000000 --- a/scripts/test_pqc.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Test script for Dilithium-5 Signature Verification -// This mimics the logic in pallets/pallet-ghost-consensus/src/functions.rs - -use pqcrypto_dilithium::dilithium5; -use pqcrypto_traits::sign::{DetachedSignature, PublicKey, SecretKey}; - -fn main() { - println!("--- Ghost PQC Verification Test ---"); - - // 1. Generate Keypair - let (pk, sk) = dilithium5::keypair(); - println!("Generated Dilithium-5 Keypair."); - println!("Public Key Size: {} bytes", pk.as_bytes().len()); - println!("Signature Size: {} bytes", dilithium5::detached_sign(b"test", &sk).as_bytes().len()); - - // 2. Sign Message - let message = b"Ghost Block #12345 Finalization"; - let sig = dilithium5::detached_sign(message, &sk); - println!("Message signed."); - - // 3. Verify - match dilithium5::verify_detached_signature(&sig, message, &pk) { - Ok(_) => println!("✅ PQC Verification Successful!"), - Err(_) => println!("❌ PQC Verification Failed!"), - } -} diff --git a/vendor/proc-macro-error/.gitignore b/vendor/proc-macro-error/.gitignore new file mode 100644 index 0000000..5e81b66 --- /dev/null +++ b/vendor/proc-macro-error/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +.fuse_hidden* diff --git a/vendor/proc-macro-error/.gitlab-ci.yml b/vendor/proc-macro-error/.gitlab-ci.yml new file mode 100644 index 0000000..d96920c --- /dev/null +++ b/vendor/proc-macro-error/.gitlab-ci.yml @@ -0,0 +1,53 @@ +stages: + - test + + +.setup_template: &setup_template + stage: test + image: debian:stable-slim + before_script: + - export CARGO_HOME="$CI_PROJECT_DIR/.cargo" + - export PATH="$PATH:$CARGO_HOME/bin" + - export RUST_BACKTRACE=full + - apt-get update > /dev/null + - apt-get install -y curl build-essential > /dev/null + - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUST_VERSION + - rustup --version + - rustc --version + - cargo --version + +.test_all_template: &test_all_template + <<: *setup_template + script: + - cargo test --all + + +test-stable: + <<: *test_all_template + variables: + RUST_VERSION: stable + +test-beta: + <<: *test_all_template + variables: + RUST_VERSION: beta + +test-nightly: + <<: *test_all_template + variables: + RUST_VERSION: nightly + + +test-1.31.0: + <<: *setup_template + script: + - cargo test --tests # skip doctests + variables: + RUST_VERSION: 1.31.0 + +test-fmt: + <<: *setup_template + script: + - cargo fmt --all -- --check + variables: + RUST_VERSION: stable diff --git a/vendor/proc-macro-error/.travis.yml b/vendor/proc-macro-error/.travis.yml new file mode 100644 index 0000000..362003f --- /dev/null +++ b/vendor/proc-macro-error/.travis.yml @@ -0,0 +1,19 @@ +language: rust +rust: + - stable + - beta + - nightly +script: + - cargo test --all +matrix: + include: + - rust: 1.31.0 + script: cargo test --tests # skip doctests + allow_failures: + - rust: nightly + fast_finish: true + + +notifications: + email: + on_success: never diff --git a/vendor/proc-macro-error/CHANGELOG.md b/vendor/proc-macro-error/CHANGELOG.md new file mode 100644 index 0000000..3c422f1 --- /dev/null +++ b/vendor/proc-macro-error/CHANGELOG.md @@ -0,0 +1,162 @@ +# v1.0.4 (2020-7-31) + +* `SpanRange` facility is now public. +* Docs have been improved. +* Introduced the `syn-error` feature so you can opt-out from the `syn` dependency. + +# v1.0.3 (2020-6-26) + +* Corrected a few typos. +* Fixed the `emit_call_site_warning` macro. + +# v1.0.2 (2020-4-9) + +* An obsolete note was removed from documentation. + +# v1.0.1 (2020-4-9) + +* `proc-macro-hack` is now well tested and supported. Not sure about `proc-macro-nested`, + please fill a request if you need it. +* Fixed `emit_call_site_error`. +* Documentation improvements. + +# v1.0.0 (2020-3-25) + +I believe the API can be considered stable because it's been a few months without +breaking changes, and I also don't think this crate will receive much further evolution. +It's perfect, admit it. + +Hence, meet the new, stable release! + +### Improvements + +* Supported nested `#[proc_macro_error]` attributes. Well, you aren't supposed to do that, + but I caught myself doing it by accident on one occasion and the behavior was... surprising. + Better to handle this smooth. + +# v0.4.12 (2020-3-23) + +* Error message on macros' misuse is now a bit more understandable. + +# v0.4.11 (2020-3-02) + +* `build.rs` no longer fails when `rustc` date could not be determined, + (thanks to [`Fabian Möller`](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/8) + for noticing and to [`Igor Gnatenko`](https://gitlab.com/CreepySkeleton/proc-macro-error/-/merge_requests/25) + for fixing). + +# v0.4.10 (2020-2-29) + +* `proc-macro-error` doesn't depend on syn\[full\] anymore, the compilation + is \~30secs faster. + +# v0.4.9 (2020-2-13) + +* New function: `append_dummy`. + +# v0.4.8 (2020-2-01) + +* Support for children messages + +# v0.4.7 (2020-1-31) + +* Now any type that implements `quote::ToTokens` can be used instead of spans. + This allows for high quality error messages. + +# v0.4.6 (2020-1-31) + +* `From<syn::Error>` implementation doesn't lose span info anymore, see + [#6](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/6). + +# v0.4.5 (2020-1-20) +Just a small intermediate release. + +* Fix some bugs. +* Populate license files into subfolders. + +# v0.4.4 (2019-11-13) +* Fix `abort_if_dirty` + warnings bug +* Allow trailing commas in macros + +# v0.4.2 (2019-11-7) +* FINALLY fixed `__pme__suggestions not found` bug + +# v0.4.1 (2019-11-7) YANKED +* Fixed `__pme__suggestions not found` bug +* Documentation improvements, links checked + +# v0.4.0 (2019-11-6) YANKED + +## New features +* "help" messages that can have their own span on nightly, they + inherit parent span on stable. + ```rust + let cond_help = if condition { Some("some help message") else { None } }; + abort!( + span, // parent span + "something's wrong, {} wrongs in total", 10; // main message + help = "here's a help for you, {}", "take it"; // unconditional help message + help =? cond_help; // conditional help message, must be Option + note = note_span => "don't forget the note, {}", "would you?" // notes can have their own span but it's effective only on nightly + ) + ``` +* Warnings via `emit_warning` and `emit_warning_call_site`. Nightly only, they're ignored on stable. +* Now `proc-macro-error` delegates to `proc_macro::Diagnostic` on nightly. + +## Breaking changes +* `MacroError` is now replaced by `Diagnostic`. Its API resembles `proc_macro::Diagnostic`. +* `Diagnostic` does not implement `From<&str/String>` so `Result<T, &str/String>::abort_or_exit()` + won't work anymore (nobody used it anyway). +* `macro_error!` macro is replaced with `diagnostic!`. + +## Improvements +* Now `proc-macro-error` renders notes exactly just like rustc does. +* We don't parse a body of a function annotated with `#[proc_macro_error]` anymore, + only looking at the signature. This should somewhat decrease expansion time for large functions. + +# v0.3.3 (2019-10-16) +* Now you can use any word instead of "help", undocumented. + +# v0.3.2 (2019-10-16) +* Introduced support for "help" messages, undocumented. + +# v0.3.0 (2019-10-8) + +## The crate has been completely rewritten from scratch! + +## Changes (most are breaking): +* Renamed macros: + * `span_error` => `abort` + * `call_site_error` => `abort_call_site` +* `filter_macro_errors` was replaced by `#[proc_macro_error]` attribute. +* `set_dummy` now takes `TokenStream` instead of `Option<TokenStream>` +* Support for multiple errors via `emit_error` and `emit_call_site_error` +* New `macro_error` macro for building errors in format=like style. +* `MacroError` API had been reconsidered. It also now implements `quote::ToTokens`. + +# v0.2.6 (2019-09-02) +* Introduce support for dummy implementations via `dummy::set_dummy` +* `multi::*` is now deprecated, will be completely rewritten in v0.3 + +# v0.2.0 (2019-08-15) + +## Breaking changes +* `trigger_error` replaced with `MacroError::trigger` and `filter_macro_error_panics` + is hidden from docs. + This is not quite a breaking change since users weren't supposed to use these functions directly anyway. +* All dependencies are updated to `v1.*`. + +## New features +* Ability to stack multiple errors via `multi::MultiMacroErrors` and emit them at once. + +## Improvements +* Now `MacroError` implements `std::fmt::Display` instead of `std::string::ToString`. +* `MacroError::span` inherent method. +* `From<MacroError> for proc_macro/proc_macro2::TokenStream` implementations. +* `AsRef/AsMut<String> for MacroError` implementations. + +# v0.1.x (2019-07-XX) + +## New features +* An easy way to report errors inside within a proc-macro via `span_error`, + `call_site_error` and `filter_macro_errors`. diff --git a/vendor/proc-macro-error/Cargo.toml b/vendor/proc-macro-error/Cargo.toml new file mode 100644 index 0000000..869585f --- /dev/null +++ b/vendor/proc-macro-error/Cargo.toml @@ -0,0 +1,56 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +edition = "2018" +name = "proc-macro-error" +version = "1.0.4" +authors = ["CreepySkeleton <creepy-skeleton@yandex.ru>"] +build = "build.rs" +description = "Almost drop-in replacement to panics in proc-macros" +readme = "README.md" +keywords = ["proc-macro", "error", "errors"] +categories = ["development-tools::procedural-macro-helpers"] +license = "MIT OR Apache-2.0" +repository = "https://gitlab.com/CreepySkeleton/proc-macro-error" +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +[dependencies.proc-macro-error-attr] +version = "=1.0.4" + +[dependencies.proc-macro2] +version = "1" + +[dependencies.quote] +version = "1" + +[dependencies.syn] +version = "1" +optional = true +default-features = false +[dev-dependencies.serde_derive] +version = "=1.0.107" + +[dev-dependencies.toml] +version = "=0.5.2" + +[dev-dependencies.trybuild] +version = "1.0.19" +features = ["diff"] +[build-dependencies.version_check] +version = "0.9" + +[features] +default = ["syn-error"] +syn-error = ["syn"] +[badges.maintenance] +status = "passively-maintained" diff --git a/vendor/proc-macro-error/Cargo.toml.orig b/vendor/proc-macro-error/Cargo.toml.orig new file mode 100644 index 0000000..5ad358d --- /dev/null +++ b/vendor/proc-macro-error/Cargo.toml.orig @@ -0,0 +1,44 @@ +[package] +name = "proc-macro-error" +version = "1.0.4" +authors = ["CreepySkeleton <creepy-skeleton@yandex.ru>"] +description = "Almost drop-in replacement to panics in proc-macros" + +repository = "https://gitlab.com/CreepySkeleton/proc-macro-error" +readme = "README.md" +keywords = ["proc-macro", "error", "errors"] +categories = ["development-tools::procedural-macro-helpers"] +license = "MIT OR Apache-2.0" + +edition = "2018" +build = "build.rs" + +[badges] +maintenance = { status = "passively-maintained" } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +quote = "1" +proc-macro2 = "1" +proc-macro-error-attr = { path = "./proc-macro-error-attr", version = "=1.0.4"} + +[dependencies.syn] +version = "1" +optional = true +default-features = false + +[dev-dependencies] +test-crate = { path = "./test-crate" } +proc-macro-hack-test = { path = "./test-crate/proc-macro-hack-test" } +trybuild = { version = "1.0.19", features = ["diff"] } +toml = "=0.5.2" # DO NOT BUMP +serde_derive = "=1.0.107" # DO NOT BUMP + +[build-dependencies] +version_check = "0.9" + +[features] +default = ["syn-error"] +syn-error = ["syn"] diff --git a/vendor/proc-macro-error/LICENSE-APACHE b/vendor/proc-macro-error/LICENSE-APACHE new file mode 100644 index 0000000..658240a --- /dev/null +++ b/vendor/proc-macro-error/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019-2020 CreepySkeleton <creepy-skeleton@yandex.ru> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/proc-macro-error/LICENSE-MIT b/vendor/proc-macro-error/LICENSE-MIT new file mode 100644 index 0000000..fc73e59 --- /dev/null +++ b/vendor/proc-macro-error/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 CreepySkeleton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/proc-macro-error/README.md b/vendor/proc-macro-error/README.md new file mode 100644 index 0000000..7fbe07c --- /dev/null +++ b/vendor/proc-macro-error/README.md @@ -0,0 +1,258 @@ +# Makes error reporting in procedural macros nice and easy + +[![travis ci](https://travis-ci.org/CreepySkeleton/proc-macro-error.svg?branch=master)](https://travis-ci.org/CreepySkeleton/proc-macro-error) +[![docs.rs](https://docs.rs/proc-macro-error/badge.svg)](https://docs.rs/proc-macro-error) +[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) + +This crate aims to make error reporting in proc-macros simple and easy to use. +Migrate from `panic!`-based errors for as little effort as possible! + +Also, you can explicitly [append a dummy token stream][crate::dummy] to your errors. + +To achieve his, this crate serves as a tiny shim around `proc_macro::Diagnostic` and +`compile_error!`. It detects the most preferable way to emit errors based on compiler's version. +When the underlying diagnostic type is finally stabilized, this crate will be simply +delegating to it, requiring no changes in your code! + +So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality +available on stable ahead of time and your error-reporting code future-proof. + +```toml +[dependencies] +proc-macro-error = "1.0" +``` + +*Supports rustc 1.31 and up* + +[Documentation and guide][guide] + +## Quick example + +Code: + +```rust +#[proc_macro] +#[proc_macro_error] +pub fn make_fn(input: TokenStream) -> TokenStream { + let mut input = TokenStream2::from(input).into_iter(); + let name = input.next().unwrap(); + if let Some(second) = input.next() { + abort! { second, + "I don't like this part!"; + note = "I see what you did there..."; + help = "I need only one part, you know?"; + } + } + + quote!( fn #name() {} ).into() +} +``` + +This is how the error is rendered in a terminal: + +<p align="center"> +<img src="https://user-images.githubusercontent.com/50968528/78830016-d3b46a80-79d6-11ea-9de2-972e8d7904ef.png" width="600"> +</p> + +And this is what your users will see in their IDE: + +<p align="center"> +<img src="https://user-images.githubusercontent.com/50968528/78830547-a9af7800-79d7-11ea-822e-59e29bda335c.png" width="600"> +</p> + +## Examples + +### Panic-like usage + +```rust +use proc_macro_error::{ + proc_macro_error, + abort, + abort_call_site, + ResultExt, + OptionExt, +}; +use proc_macro::TokenStream; +use syn::{DeriveInput, parse_macro_input}; +use quote::quote; + +// This is your main entry point +#[proc_macro] +// This attribute *MUST* be placed on top of the #[proc_macro] function +#[proc_macro_error] +pub fn make_answer(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + if let Err(err) = some_logic(&input) { + // we've got a span to blame, let's use it + // This immediately aborts the proc-macro and shows the error + // + // You can use `proc_macro::Span`, `proc_macro2::Span`, and + // anything that implements `quote::ToTokens` (almost every type from + // `syn` and `proc_macro2`) + abort!(err, "You made an error, go fix it: {}", err.msg); + } + + // `Result` has some handy shortcuts if your error type implements + // `Into<Diagnostic>`. `Option` has one unconditionally. + more_logic(&input).expect_or_abort("What a careless user, behave!"); + + if !more_logic_for_logic_god(&input) { + // We don't have an exact location this time, + // so just highlight the proc-macro invocation itself + abort_call_site!( + "Bad, bad user! Now go stand in the corner and think about what you did!"); + } + + // Now all the processing is done, return `proc_macro::TokenStream` + quote!(/* stuff */).into() +} +``` + +### `proc_macro::Diagnostic`-like usage + +```rust +use proc_macro_error::*; +use proc_macro::TokenStream; +use syn::{spanned::Spanned, DeriveInput, ItemStruct, Fields, Attribute , parse_macro_input}; +use quote::quote; + +fn process_attrs(attrs: &[Attribute]) -> Vec<Attribute> { + attrs + .iter() + .filter_map(|attr| match process_attr(attr) { + Ok(res) => Some(res), + Err(msg) => { + emit_error!(attr, "Invalid attribute: {}", msg); + None + } + }) + .collect() +} + +fn process_fields(_attrs: &Fields) -> Vec<TokenStream> { + // processing fields in pretty much the same way as attributes + unimplemented!() +} + +#[proc_macro] +#[proc_macro_error] +pub fn make_answer(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + let attrs = process_attrs(&input.attrs); + + // abort right now if some errors were encountered + // at the attributes processing stage + abort_if_dirty(); + + let fields = process_fields(&input.fields); + + // no need to think about emitted errors + // #[proc_macro_error] will handle them for you + // + // just return a TokenStream as you normally would + quote!(/* stuff */).into() +} +``` + +## Real world examples + +* [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) + (abort-like usage) +* [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) + +## Limitations + +- Warnings are emitted only on nightly, they are ignored on stable. +- "help" suggestions can't have their own span info on stable, + (essentially inheriting the parent span). +- If your macro happens to trigger a panic, no errors will be displayed. This is not a + technical limitation but rather intentional design. `panic` is not for error reporting. + +## MSRV policy + +`proc_macro_error` will always be compatible with proc-macro Holy Trinity: +`proc_macro2`, `syn`, `quote` crates. In other words, if the Trinity is available +to you - `proc_macro_error` is available too. + +> **Important!** +> +> If you want to use `#[proc_macro_error]` with `synstructure`, you're going +> to have to put the attribute inside the `decl_derive!` invocation. Unfortunately, +> due to some bug in pre-1.34 rustc, putting proc-macro attributes inside macro +> invocations doesn't work, so your MSRV is effectively 1.34. + +## Motivation + +Error handling in proc-macros sucks. There's not much of a choice today: +you either "bubble up" the error up to the top-level of the macro and convert it to +a [`compile_error!`][compl_err] invocation or just use a good old panic. Both these ways suck: + +- Former sucks because it's quite redundant to unroll a proper error handling + just for critical errors that will crash the macro anyway; so people mostly + choose not to bother with it at all and use panic. Simple `.expect` is too tempting. + + Also, if you do decide to implement this `Result`-based architecture in your macro + you're going to have to rewrite it entirely once [`proc_macro::Diagnostic`][] is finally + stable. Not cool. + +- Later sucks because there's no way to carry out the span info via `panic!`. + `rustc` will highlight the invocation itself but not some specific token inside it. + + Furthermore, panics aren't for error-reporting at all; panics are for bug-detecting + (like unwrapping on `None` or out-of-range indexing) or for early development stages + when you need a prototype ASAP so error handling can wait. Mixing these usages only + messes things up. + +- There is [`proc_macro::Diagnostic`][] which is awesome but it has been experimental + for more than a year and is unlikely to be stabilized any time soon. + + This crate's API is intentionally designed to be compatible with `proc_macro::Diagnostic` + and delegates to it whenever possible. Once `Diagnostics` is stable this crate + will **always** delegate to it, no code changes will be required on user side. + +That said, we need a solution, but this solution must meet these conditions: + +- It must be better than `panic!`. The main point: it must offer a way to carry the span information + over to user. +- It must take as little effort as possible to migrate from `panic!`. Ideally, a new + macro with similar semantics plus ability to carry out span info. +- It must maintain compatibility with [`proc_macro::Diagnostic`][] . +- **It must be usable on stable**. + +This crate aims to provide such a mechanism. All you have to do is annotate your top-level +`#[proc_macro]` function with `#[proc_macro_error]` attribute and change panics to +[`abort!`]/[`abort_call_site!`] where appropriate, see [the Guide][guide]. + +## Disclaimer +Please note that **this crate is not intended to be used in any way other +than error reporting in procedural macros**, use `Result` and `?` (possibly along with one of the +many helpers out there) for anything else. + +<br> + +#### License + +<sup> +Licensed under either of <a href="LICENSE-APACHE">Apache License, Version +2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option. +</sup> + +<br> + +<sub> +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. +</sub> + + +[compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html +[`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html + +[crate::dummy]: https://docs.rs/proc-macro-error/1/proc_macro_error/dummy/index.html +[crate::multi]: https://docs.rs/proc-macro-error/1/proc_macro_error/multi/index.html + +[`abort_call_site!`]: https://docs.rs/proc-macro-error/1/proc_macro_error/macro.abort_call_site.html +[`abort!`]: https://docs.rs/proc-macro-error/1/proc_macro_error/macro.abort.html +[guide]: https://docs.rs/proc-macro-error diff --git a/vendor/proc-macro-error/build.rs b/vendor/proc-macro-error/build.rs new file mode 100644 index 0000000..3c1196f --- /dev/null +++ b/vendor/proc-macro-error/build.rs @@ -0,0 +1,11 @@ +fn main() { + if !version_check::is_feature_flaggable().unwrap_or(false) { + println!("cargo:rustc-cfg=use_fallback"); + } + + if version_check::is_max_version("1.38.0").unwrap_or(false) + || !version_check::Channel::read().unwrap().is_stable() + { + println!("cargo:rustc-cfg=skip_ui_tests"); + } +} diff --git a/vendor/proc-macro-error/src/diagnostic.rs b/vendor/proc-macro-error/src/diagnostic.rs new file mode 100644 index 0000000..983e617 --- /dev/null +++ b/vendor/proc-macro-error/src/diagnostic.rs @@ -0,0 +1,349 @@ +use crate::{abort_now, check_correctness, sealed::Sealed, SpanRange}; +use proc_macro2::Span; +use proc_macro2::TokenStream; + +use quote::{quote_spanned, ToTokens}; + +/// Represents a diagnostic level +/// +/// # Warnings +/// +/// Warnings are ignored on stable/beta +#[derive(Debug, PartialEq)] +pub enum Level { + Error, + Warning, + #[doc(hidden)] + NonExhaustive, +} + +/// Represents a single diagnostic message +#[derive(Debug)] +pub struct Diagnostic { + pub(crate) level: Level, + pub(crate) span_range: SpanRange, + pub(crate) msg: String, + pub(crate) suggestions: Vec<(SuggestionKind, String, Option<SpanRange>)>, + pub(crate) children: Vec<(SpanRange, String)>, +} + +/// A collection of methods that do not exist in `proc_macro::Diagnostic` +/// but still useful to have around. +/// +/// This trait is sealed and cannot be implemented outside of `proc_macro_error`. +pub trait DiagnosticExt: Sealed { + /// Create a new diagnostic message that points to the `span_range`. + /// + /// This function is the same as `Diagnostic::spanned` but produces considerably + /// better error messages for multi-token spans on stable. + fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self; + + /// Add another error message to self such that it will be emitted right after + /// the main message. + /// + /// This function is the same as `Diagnostic::span_error` but produces considerably + /// better error messages for multi-token spans on stable. + fn span_range_error(self, span_range: SpanRange, msg: String) -> Self; + + /// Attach a "help" note to your main message, the note will have it's own span on nightly. + /// + /// This function is the same as `Diagnostic::span_help` but produces considerably + /// better error messages for multi-token spans on stable. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + fn span_range_help(self, span_range: SpanRange, msg: String) -> Self; + + /// Attach a note to your main message, the note will have it's own span on nightly. + /// + /// This function is the same as `Diagnostic::span_note` but produces considerably + /// better error messages for multi-token spans on stable. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + fn span_range_note(self, span_range: SpanRange, msg: String) -> Self; +} + +impl DiagnosticExt for Diagnostic { + fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self { + Diagnostic { + level, + span_range, + msg: message, + suggestions: vec![], + children: vec![], + } + } + + fn span_range_error(mut self, span_range: SpanRange, msg: String) -> Self { + self.children.push((span_range, msg)); + self + } + + fn span_range_help(mut self, span_range: SpanRange, msg: String) -> Self { + self.suggestions + .push((SuggestionKind::Help, msg, Some(span_range))); + self + } + + fn span_range_note(mut self, span_range: SpanRange, msg: String) -> Self { + self.suggestions + .push((SuggestionKind::Note, msg, Some(span_range))); + self + } +} + +impl Diagnostic { + /// Create a new diagnostic message that points to `Span::call_site()` + pub fn new(level: Level, message: String) -> Self { + Diagnostic::spanned(Span::call_site(), level, message) + } + + /// Create a new diagnostic message that points to the `span` + pub fn spanned(span: Span, level: Level, message: String) -> Self { + Diagnostic::spanned_range( + SpanRange { + first: span, + last: span, + }, + level, + message, + ) + } + + /// Add another error message to self such that it will be emitted right after + /// the main message. + pub fn span_error(self, span: Span, msg: String) -> Self { + self.span_range_error( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a "help" note to your main message, the note will have it's own span on nightly. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + pub fn span_help(self, span: Span, msg: String) -> Self { + self.span_range_help( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a "help" note to your main message. + pub fn help(mut self, msg: String) -> Self { + self.suggestions.push((SuggestionKind::Help, msg, None)); + self + } + + /// Attach a note to your main message, the note will have it's own span on nightly. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + pub fn span_note(self, span: Span, msg: String) -> Self { + self.span_range_note( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a note to your main message + pub fn note(mut self, msg: String) -> Self { + self.suggestions.push((SuggestionKind::Note, msg, None)); + self + } + + /// The message of main warning/error (no notes attached) + pub fn message(&self) -> &str { + &self.msg + } + + /// Abort the proc-macro's execution and display the diagnostic. + /// + /// # Warnings + /// + /// Warnings are not emitted on stable and beta, but this function will abort anyway. + pub fn abort(self) -> ! { + self.emit(); + abort_now() + } + + /// Display the diagnostic while not aborting macro execution. + /// + /// # Warnings + /// + /// Warnings are ignored on stable/beta + pub fn emit(self) { + check_correctness(); + crate::imp::emit_diagnostic(self); + } +} + +/// **NOT PUBLIC API! NOTHING TO SEE HERE!!!** +#[doc(hidden)] +impl Diagnostic { + pub fn span_suggestion(self, span: Span, suggestion: &str, msg: String) -> Self { + match suggestion { + "help" | "hint" => self.span_help(span, msg), + _ => self.span_note(span, msg), + } + } + + pub fn suggestion(self, suggestion: &str, msg: String) -> Self { + match suggestion { + "help" | "hint" => self.help(msg), + _ => self.note(msg), + } + } +} + +impl ToTokens for Diagnostic { + fn to_tokens(&self, ts: &mut TokenStream) { + use std::borrow::Cow; + + fn ensure_lf(buf: &mut String, s: &str) { + if s.ends_with('\n') { + buf.push_str(s); + } else { + buf.push_str(s); + buf.push('\n'); + } + } + + fn diag_to_tokens( + span_range: SpanRange, + level: &Level, + msg: &str, + suggestions: &[(SuggestionKind, String, Option<SpanRange>)], + ) -> TokenStream { + if *level == Level::Warning { + return TokenStream::new(); + } + + let message = if suggestions.is_empty() { + Cow::Borrowed(msg) + } else { + let mut message = String::new(); + ensure_lf(&mut message, msg); + message.push('\n'); + + for (kind, note, _span) in suggestions { + message.push_str(" = "); + message.push_str(kind.name()); + message.push_str(": "); + ensure_lf(&mut message, note); + } + message.push('\n'); + + Cow::Owned(message) + }; + + let mut msg = proc_macro2::Literal::string(&message); + msg.set_span(span_range.last); + let group = quote_spanned!(span_range.last=> { #msg } ); + quote_spanned!(span_range.first=> compile_error!#group) + } + + ts.extend(diag_to_tokens( + self.span_range, + &self.level, + &self.msg, + &self.suggestions, + )); + ts.extend( + self.children + .iter() + .map(|(span_range, msg)| diag_to_tokens(*span_range, &Level::Error, &msg, &[])), + ); + } +} + +#[derive(Debug)] +pub(crate) enum SuggestionKind { + Help, + Note, +} + +impl SuggestionKind { + fn name(&self) -> &'static str { + match self { + SuggestionKind::Note => "note", + SuggestionKind::Help => "help", + } + } +} + +#[cfg(feature = "syn-error")] +impl From<syn::Error> for Diagnostic { + fn from(err: syn::Error) -> Self { + use proc_macro2::{Delimiter, TokenTree}; + + fn gut_error(ts: &mut impl Iterator<Item = TokenTree>) -> Option<(SpanRange, String)> { + let first = match ts.next() { + // compile_error + None => return None, + Some(tt) => tt.span(), + }; + ts.next().unwrap(); // ! + + let lit = match ts.next().unwrap() { + TokenTree::Group(group) => { + // Currently `syn` builds `compile_error!` invocations + // exclusively in `ident{"..."}` (braced) form which is not + // followed by `;` (semicolon). + // + // But if it changes to `ident("...");` (parenthesized) + // or `ident["..."];` (bracketed) form, + // we will need to skip the `;` as well. + // Highly unlikely, but better safe than sorry. + + if group.delimiter() == Delimiter::Parenthesis + || group.delimiter() == Delimiter::Bracket + { + ts.next().unwrap(); // ; + } + + match group.stream().into_iter().next().unwrap() { + TokenTree::Literal(lit) => lit, + _ => unreachable!(), + } + } + _ => unreachable!(), + }; + + let last = lit.span(); + let mut msg = lit.to_string(); + + // "abc" => abc + msg.pop(); + msg.remove(0); + + Some((SpanRange { first, last }, msg)) + } + + let mut ts = err.to_compile_error().into_iter(); + + let (span_range, msg) = gut_error(&mut ts).unwrap(); + let mut res = Diagnostic::spanned_range(span_range, Level::Error, msg); + + while let Some((span_range, msg)) = gut_error(&mut ts) { + res = res.span_range_error(span_range, msg); + } + + res + } +} diff --git a/vendor/proc-macro-error/src/dummy.rs b/vendor/proc-macro-error/src/dummy.rs new file mode 100644 index 0000000..571a595 --- /dev/null +++ b/vendor/proc-macro-error/src/dummy.rs @@ -0,0 +1,150 @@ +//! Facility to emit dummy implementations (or whatever) in case +//! an error happen. +//! +//! `compile_error!` does not abort a compilation right away. This means +//! `rustc` doesn't just show you the error and abort, it carries on the +//! compilation process looking for other errors to report. +//! +//! Let's consider an example: +//! +//! ```rust,ignore +//! use proc_macro::TokenStream; +//! use proc_macro_error::*; +//! +//! trait MyTrait { +//! fn do_thing(); +//! } +//! +//! // this proc macro is supposed to generate MyTrait impl +//! #[proc_macro_derive(MyTrait)] +//! #[proc_macro_error] +//! fn example(input: TokenStream) -> TokenStream { +//! // somewhere deep inside +//! abort!(span, "something's wrong"); +//! +//! // this implementation will be generated if no error happened +//! quote! { +//! impl MyTrait for #name { +//! fn do_thing() {/* whatever */} +//! } +//! } +//! } +//! +//! // ================ +//! // in main.rs +//! +//! // this derive triggers an error +//! #[derive(MyTrait)] // first BOOM! +//! struct Foo; +//! +//! fn main() { +//! Foo::do_thing(); // second BOOM! +//! } +//! ``` +//! +//! The problem is: the generated token stream contains only `compile_error!` +//! invocation, the impl was not generated. That means user will see two compilation +//! errors: +//! +//! ```text +//! error: something's wrong +//! --> $DIR/probe.rs:9:10 +//! | +//! 9 |#[proc_macro_derive(MyTrait)] +//! | ^^^^^^^ +//! +//! error[E0599]: no function or associated item named `do_thing` found for type `Foo` in the current scope +//! --> src\main.rs:3:10 +//! | +//! 1 | struct Foo; +//! | ----------- function or associated item `do_thing` not found for this +//! 2 | fn main() { +//! 3 | Foo::do_thing(); // second BOOM! +//! | ^^^^^^^^ function or associated item not found in `Foo` +//! ``` +//! +//! But the second error is meaningless! We definitely need to fix this. +//! +//! Most used approach in cases like this is "dummy implementation" - +//! omit `impl MyTrait for #name` and fill functions bodies with `unimplemented!()`. +//! +//! This is how you do it: +//! +//! ```rust,ignore +//! use proc_macro::TokenStream; +//! use proc_macro_error::*; +//! +//! trait MyTrait { +//! fn do_thing(); +//! } +//! +//! // this proc macro is supposed to generate MyTrait impl +//! #[proc_macro_derive(MyTrait)] +//! #[proc_macro_error] +//! fn example(input: TokenStream) -> TokenStream { +//! // first of all - we set a dummy impl which will be appended to +//! // `compile_error!` invocations in case a trigger does happen +//! set_dummy(quote! { +//! impl MyTrait for #name { +//! fn do_thing() { unimplemented!() } +//! } +//! }); +//! +//! // somewhere deep inside +//! abort!(span, "something's wrong"); +//! +//! // this implementation will be generated if no error happened +//! quote! { +//! impl MyTrait for #name { +//! fn do_thing() {/* whatever */} +//! } +//! } +//! } +//! +//! // ================ +//! // in main.rs +//! +//! // this derive triggers an error +//! #[derive(MyTrait)] // first BOOM! +//! struct Foo; +//! +//! fn main() { +//! Foo::do_thing(); // no more errors! +//! } +//! ``` + +use proc_macro2::TokenStream; +use std::cell::RefCell; + +use crate::check_correctness; + +thread_local! { + static DUMMY_IMPL: RefCell<Option<TokenStream>> = RefCell::new(None); +} + +/// Sets dummy token stream which will be appended to `compile_error!(msg);...` +/// invocations in case you'll emit any errors. +/// +/// See [guide](../index.html#guide). +pub fn set_dummy(dummy: TokenStream) -> Option<TokenStream> { + check_correctness(); + DUMMY_IMPL.with(|old_dummy| old_dummy.replace(Some(dummy))) +} + +/// Same as [`set_dummy`] but, instead of resetting, appends tokens to the +/// existing dummy (if any). Behaves as `set_dummy` if no dummy is present. +pub fn append_dummy(dummy: TokenStream) { + check_correctness(); + DUMMY_IMPL.with(|old_dummy| { + let mut cell = old_dummy.borrow_mut(); + if let Some(ts) = cell.as_mut() { + ts.extend(dummy); + } else { + *cell = Some(dummy); + } + }); +} + +pub(crate) fn cleanup() -> Option<TokenStream> { + DUMMY_IMPL.with(|old_dummy| old_dummy.replace(None)) +} diff --git a/vendor/proc-macro-error/src/imp/delegate.rs b/vendor/proc-macro-error/src/imp/delegate.rs new file mode 100644 index 0000000..07def2b --- /dev/null +++ b/vendor/proc-macro-error/src/imp/delegate.rs @@ -0,0 +1,69 @@ +//! This implementation uses [`proc_macro::Diagnostic`], nightly only. + +use std::cell::Cell; + +use proc_macro::{Diagnostic as PDiag, Level as PLevel}; + +use crate::{ + abort_now, check_correctness, + diagnostic::{Diagnostic, Level, SuggestionKind}, +}; + +pub fn abort_if_dirty() { + check_correctness(); + if IS_DIRTY.with(|c| c.get()) { + abort_now() + } +} + +pub(crate) fn cleanup() -> Vec<Diagnostic> { + IS_DIRTY.with(|c| c.set(false)); + vec![] +} + +pub(crate) fn emit_diagnostic(diag: Diagnostic) { + let Diagnostic { + level, + span_range, + msg, + suggestions, + children, + } = diag; + + let span = span_range.collapse().unwrap(); + + let level = match level { + Level::Warning => PLevel::Warning, + Level::Error => { + IS_DIRTY.with(|c| c.set(true)); + PLevel::Error + } + _ => unreachable!(), + }; + + let mut res = PDiag::spanned(span, level, msg); + + for (kind, msg, span) in suggestions { + res = match (kind, span) { + (SuggestionKind::Note, Some(span_range)) => { + res.span_note(span_range.collapse().unwrap(), msg) + } + (SuggestionKind::Help, Some(span_range)) => { + res.span_help(span_range.collapse().unwrap(), msg) + } + (SuggestionKind::Note, None) => res.note(msg), + (SuggestionKind::Help, None) => res.help(msg), + } + } + + for (span_range, msg) in children { + let span = span_range.collapse().unwrap(); + res = res.span_error(span, msg); + } + + res.emit() +} + +thread_local! { + static IS_DIRTY: Cell<bool> = Cell::new(false); +} diff --git a/vendor/proc-macro-error/src/imp/fallback.rs b/vendor/proc-macro-error/src/imp/fallback.rs new file mode 100644 index 0000000..ad1f730 --- /dev/null +++ b/vendor/proc-macro-error/src/imp/fallback.rs @@ -0,0 +1,30 @@ +//! This implementation uses self-written stable facilities. + +use crate::{ + abort_now, check_correctness, + diagnostic::{Diagnostic, Level}, +}; +use std::cell::RefCell; + +pub fn abort_if_dirty() { + check_correctness(); + ERR_STORAGE.with(|storage| { + if !storage.borrow().is_empty() { + abort_now() + } + }); +} + +pub(crate) fn cleanup() -> Vec<Diagnostic> { + ERR_STORAGE.with(|storage| storage.replace(Vec::new())) +} + +pub(crate) fn emit_diagnostic(diag: Diagnostic) { + if diag.level == Level::Error { + ERR_STORAGE.with(|storage| storage.borrow_mut().push(diag)); + } +} + +thread_local! { + static ERR_STORAGE: RefCell<Vec<Diagnostic>> = RefCell::new(Vec::new()); +} diff --git a/vendor/proc-macro-error/src/lib.rs b/vendor/proc-macro-error/src/lib.rs new file mode 100644 index 0000000..3c48ac5 --- /dev/null +++ b/vendor/proc-macro-error/src/lib.rs @@ -0,0 +1,560 @@ +//! # proc-macro-error +//! +//! This crate aims to make error reporting in proc-macros simple and easy to use. +//! Migrate from `panic!`-based errors for as little effort as possible! +//! +//! (Also, you can explicitly [append a dummy token stream](dummy/index.html) to your errors). +//! +//! To achieve his, this crate serves as a tiny shim around `proc_macro::Diagnostic` and +//! `compile_error!`. It detects the best way of emitting available based on compiler's version. +//! When the underlying diagnostic type is finally stabilized, this crate will simply be +//! delegating to it requiring no changes in your code! +//! +//! So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality +//! available on stable ahead of time *and* your error-reporting code future-proof. +//! +//! ## Cargo features +//! +//! This crate provides *enabled by default* `syn-error` feature that gates +//! `impl From<syn::Error> for Diagnostic` conversion. If you don't use `syn` and want +//! to cut off some of compilation time, you can disable it via +//! +//! ```toml +//! [dependencies] +//! proc-macro-error = { version = "1", default-features = false } +//! ``` +//! +//! ***Please note that disabling this feature makes sense only if you don't depend on `syn` +//! directly or indirectly, and you very likely do.** +//! +//! ## Real world examples +//! +//! * [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) +//! (abort-like usage) +//! * [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) +//! +//! ## Limitations +//! +//! - Warnings are emitted only on nightly, they are ignored on stable. +//! - "help" suggestions can't have their own span info on stable, +//! (essentially inheriting the parent span). +//! - If a panic occurs somewhere in your macro no errors will be displayed. This is not a +//! technical limitation but rather intentional design. `panic` is not for error reporting. +//! +//! ### `#[proc_macro_error]` attribute +//! +//! **This attribute MUST be present on the top level of your macro** (the function +//! annotated with any of `#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`). +//! +//! This attribute performs the setup and cleanup necessary to make things work. +//! +//! In most cases you'll need the simple `#[proc_macro_error]` form without any +//! additional settings. Feel free to [skip the "Syntax" section](#macros). +//! +//! #### Syntax +//! +//! `#[proc_macro_error]` or `#[proc_macro_error(settings...)]`, where `settings...` +//! is a comma-separated list of: +//! +//! - `proc_macro_hack`: +//! +//! In order to correctly cooperate with `#[proc_macro_hack]`, `#[proc_macro_error]` +//! attribute must be placed *before* (above) it, like this: +//! +//! ```no_run +//! # use proc_macro2::TokenStream; +//! # const IGNORE: &str = " +//! #[proc_macro_error] +//! #[proc_macro_hack] +//! #[proc_macro] +//! # "; +//! fn my_macro(input: TokenStream) -> TokenStream { +//! unimplemented!() +//! } +//! ``` +//! +//! If, for some reason, you can't place it like that you can use +//! `#[proc_macro_error(proc_macro_hack)]` instead. +//! +//! # Note +//! +//! If `proc-macro-hack` was detected (by any means) `allow_not_macro` +//! and `assert_unwind_safe` will be applied automatically. +//! +//! - `allow_not_macro`: +//! +//! By default, the attribute checks that it's applied to a proc-macro. +//! If none of `#[proc_macro]`, `#[proc_macro_derive]` nor `#[proc_macro_attribute]` are +//! present it will panic. It's the intention - this crate is supposed to be used only with +//! proc-macros. +//! +//! This setting is made to bypass the check, useful in certain circumstances. +//! +//! Pay attention: the function this attribute is applied to must return +//! `proc_macro::TokenStream`. +//! +//! This setting is implied if `proc-macro-hack` was detected. +//! +//! - `assert_unwind_safe`: +//! +//! By default, your code must be [unwind safe]. If your code is not unwind safe, +//! but you believe it's correct, you can use this setting to bypass the check. +//! You would need this for code that uses `lazy_static` or `thread_local` with +//! `Cell/RefCell` inside (and the like). +//! +//! This setting is implied if `#[proc_macro_error]` is applied to a function +//! marked as `#[proc_macro]`, `#[proc_macro_derive]` or `#[proc_macro_attribute]`. +//! +//! This setting is also implied if `proc-macro-hack` was detected. +//! +//! ## Macros +//! +//! Most of the time you want to use the macros. Syntax is described in the next section below. +//! +//! You'll need to decide how you want to emit errors: +//! +//! * Emit the error and abort. Very much panic-like usage. Served by [`abort!`] and +//! [`abort_call_site!`]. +//! * Emit the error but do not abort right away, looking for other errors to report. +//! Served by [`emit_error!`] and [`emit_call_site_error!`]. +//! +//! You **can** mix these usages. +//! +//! `abort` and `emit_error` take a "source span" as the first argument. This source +//! will be used to highlight the place the error originates from. It must be one of: +//! +//! * *Something* that implements [`ToTokens`] (most types in `syn` and `proc-macro2` do). +//! This source is the preferable one since it doesn't lose span information on multi-token +//! spans, see [this issue](https://gitlab.com/CreepySkeleton/proc-macro-error/-/issues/6) +//! for details. +//! * [`proc_macro::Span`] +//! * [`proc-macro2::Span`] +//! +//! The rest is your message in format-like style. +//! +//! See [the next section](#syntax-1) for detailed syntax. +//! +//! - [`abort!`]: +//! +//! Very much panic-like usage - abort right away and show the error. +//! Expands to [`!`] (never type). +//! +//! - [`abort_call_site!`]: +//! +//! Shortcut for `abort!(Span::call_site(), ...)`. Expands to [`!`] (never type). +//! +//! - [`emit_error!`]: +//! +//! [`proc_macro::Diagnostic`]-like usage - emit the error but keep going, +//! looking for other errors to report. +//! The compilation will fail nonetheless. Expands to [`()`] (unit type). +//! +//! - [`emit_call_site_error!`]: +//! +//! Shortcut for `emit_error!(Span::call_site(), ...)`. Expands to [`()`] (unit type). +//! +//! - [`emit_warning!`]: +//! +//! Like `emit_error!` but emit a warning instead of error. The compilation won't fail +//! because of warnings. +//! Expands to [`()`] (unit type). +//! +//! **Beware**: warnings are nightly only, they are completely ignored on stable. +//! +//! - [`emit_call_site_warning!`]: +//! +//! Shortcut for `emit_warning!(Span::call_site(), ...)`. Expands to [`()`] (unit type). +//! +//! - [`diagnostic`]: +//! +//! Build an instance of `Diagnostic` in format-like style. +//! +//! #### Syntax +//! +//! All the macros have pretty much the same syntax: +//! +//! 1. ```ignore +//! abort!(single_expr) +//! ``` +//! Shortcut for `Diagnostic::from(expr).abort()`. +//! +//! 2. ```ignore +//! abort!(span, message) +//! ``` +//! The first argument is an expression the span info should be taken from. +//! +//! The second argument is the error message, it must implement [`ToString`]. +//! +//! 3. ```ignore +//! abort!(span, format_literal, format_args...) +//! ``` +//! +//! This form is pretty much the same as 2, except `format!(format_literal, format_args...)` +//! will be used to for the message instead of [`ToString`]. +//! +//! That's it. `abort!`, `emit_warning`, `emit_error` share this exact syntax. +//! +//! `abort_call_site!`, `emit_call_site_warning`, `emit_call_site_error` lack 1 form +//! and do not take span in 2'th and 3'th forms. Those are essentially shortcuts for +//! `macro!(Span::call_site(), args...)`. +//! +//! `diagnostic!` requires a [`Level`] instance between `span` and second argument +//! (1'th form is the same). +//! +//! > **Important!** +//! > +//! > If you have some type from `proc_macro` or `syn` to point to, do not call `.span()` +//! > on it but rather use it directly: +//! > ```no_run +//! > # use proc_macro_error::abort; +//! > # let input = proc_macro2::TokenStream::new(); +//! > let ty: syn::Type = syn::parse2(input).unwrap(); +//! > abort!(ty, "BOOM"); +//! > // ^^ <-- avoid .span() +//! > ``` +//! > +//! > `.span()` calls work too, but you may experience regressions in message quality. +//! +//! #### Note attachments +//! +//! 3. Every macro can have "note" attachments (only 2 and 3 form). +//! ```ignore +//! let opt_help = if have_some_info { Some("did you mean `this`?") } else { None }; +//! +//! abort!( +//! span, message; // <--- attachments start with `;` (semicolon) +//! +//! help = "format {} {}", "arg1", "arg2"; // <--- every attachment ends with `;`, +//! // maybe except the last one +//! +//! note = "to_string"; // <--- one arg uses `.to_string()` instead of `format!()` +//! +//! yay = "I see what {} did here", "you"; // <--- "help =" and "hint =" are mapped +//! // to Diagnostic::help, +//! // anything else is Diagnostic::note +//! +//! wow = note_span => "custom span"; // <--- attachments can have their own span +//! // it takes effect only on nightly though +//! +//! hint =? opt_help; // <-- "optional" attachment, get displayed only if `Some` +//! // must be single `Option` expression +//! +//! note =? note_span => opt_help // <-- optional attachments can have custom spans too +//! ); +//! ``` +//! + +//! ### Diagnostic type +//! +//! [`Diagnostic`] type is intentionally designed to be API compatible with [`proc_macro::Diagnostic`]. +//! Not all API is implemented, only the part that can be reasonably implemented on stable. +//! +//! +//! [`abort!`]: macro.abort.html +//! [`abort_call_site!`]: macro.abort_call_site.html +//! [`emit_warning!`]: macro.emit_warning.html +//! [`emit_error!`]: macro.emit_error.html +//! [`emit_call_site_warning!`]: macro.emit_call_site_error.html +//! [`emit_call_site_error!`]: macro.emit_call_site_warning.html +//! [`diagnostic!`]: macro.diagnostic.html +//! [`Diagnostic`]: struct.Diagnostic.html +//! +//! [`proc_macro::Span`]: https://doc.rust-lang.org/proc_macro/struct.Span.html +//! [`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html +//! +//! [unwind safe]: https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html#what-is-unwind-safety +//! [`!`]: https://doc.rust-lang.org/std/primitive.never.html +//! [`()`]: https://doc.rust-lang.org/std/primitive.unit.html +//! [`ToString`]: https://doc.rust-lang.org/std/string/trait.ToString.html +//! +//! [`proc-macro2::Span`]: https://docs.rs/proc-macro2/1.0.10/proc_macro2/struct.Span.html +//! [`ToTokens`]: https://docs.rs/quote/1.0.3/quote/trait.ToTokens.html +//! + +#![cfg_attr(not(use_fallback), feature(proc_macro_diagnostic))] +#![forbid(unsafe_code)] +#![allow(clippy::needless_doctest_main)] + +extern crate proc_macro; + +pub use crate::{ + diagnostic::{Diagnostic, DiagnosticExt, Level}, + dummy::{append_dummy, set_dummy}, +}; +pub use proc_macro_error_attr::proc_macro_error; + +use proc_macro2::Span; +use quote::{quote, ToTokens}; + +use std::cell::Cell; +use std::panic::{catch_unwind, resume_unwind, UnwindSafe}; + +pub mod dummy; + +mod diagnostic; +mod macros; +mod sealed; + +#[cfg(use_fallback)] +#[path = "imp/fallback.rs"] +mod imp; + +#[cfg(not(use_fallback))] +#[path = "imp/delegate.rs"] +mod imp; + +#[derive(Debug, Clone, Copy)] +pub struct SpanRange { + pub first: Span, + pub last: Span, +} + +impl SpanRange { + /// Create a range with the `first` and `last` spans being the same. + pub fn single_span(span: Span) -> Self { + SpanRange { + first: span, + last: span, + } + } + + /// Create a `SpanRange` resolving at call site. + pub fn call_site() -> Self { + SpanRange::single_span(Span::call_site()) + } + + /// Construct span range from a `TokenStream`. This method always preserves all the + /// range. + /// + /// ### Note + /// + /// If the stream is empty, the result is `SpanRange::call_site()`. If the stream + /// consists of only one `TokenTree`, the result is `SpanRange::single_span(tt.span())` + /// that doesn't lose anything. + pub fn from_tokens(ts: &dyn ToTokens) -> Self { + let mut spans = ts.to_token_stream().into_iter().map(|tt| tt.span()); + let first = spans.next().unwrap_or_else(|| Span::call_site()); + let last = spans.last().unwrap_or(first); + + SpanRange { first, last } + } + + /// Join two span ranges. The resulting range will start at `self.first` and end at + /// `other.last`. + pub fn join_range(self, other: SpanRange) -> Self { + SpanRange { + first: self.first, + last: other.last, + } + } + + /// Collapse the range into single span, preserving as much information as possible. + pub fn collapse(self) -> Span { + self.first.join(self.last).unwrap_or(self.first) + } +} + +/// This traits expands `Result<T, Into<Diagnostic>>` with some handy shortcuts. +pub trait ResultExt { + type Ok; + + /// Behaves like `Result::unwrap`: if self is `Ok` yield the contained value, + /// otherwise abort macro execution via `abort!`. + fn unwrap_or_abort(self) -> Self::Ok; + + /// Behaves like `Result::expect`: if self is `Ok` yield the contained value, + /// otherwise abort macro execution via `abort!`. + /// If it aborts then resulting error message will be preceded with `message`. + fn expect_or_abort(self, msg: &str) -> Self::Ok; +} + +/// This traits expands `Option` with some handy shortcuts. +pub trait OptionExt { + type Some; + + /// Behaves like `Option::expect`: if self is `Some` yield the contained value, + /// otherwise abort macro execution via `abort_call_site!`. + /// If it aborts the `message` will be used for [`compile_error!`][compl_err] invocation. + /// + /// [compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html + fn expect_or_abort(self, msg: &str) -> Self::Some; +} + +/// Abort macro execution and display all the emitted errors, if any. +/// +/// Does nothing if no errors were emitted (warnings do not count). +pub fn abort_if_dirty() { + imp::abort_if_dirty(); +} + +impl<T, E: Into<Diagnostic>> ResultExt for Result<T, E> { + type Ok = T; + + fn unwrap_or_abort(self) -> T { + match self { + Ok(res) => res, + Err(e) => e.into().abort(), + } + } + + fn expect_or_abort(self, message: &str) -> T { + match self { + Ok(res) => res, + Err(e) => { + let mut e = e.into(); + e.msg = format!("{}: {}", message, e.msg); + e.abort() + } + } + } +} + +impl<T> OptionExt for Option<T> { + type Some = T; + + fn expect_or_abort(self, message: &str) -> T { + match self { + Some(res) => res, + None => abort_call_site!(message), + } + } +} + +/// This is the entry point for a proc-macro. +/// +/// **NOT PUBLIC API, SUBJECT TO CHANGE WITHOUT ANY NOTICE** +#[doc(hidden)] +pub fn entry_point<F>(f: F, proc_macro_hack: bool) -> proc_macro::TokenStream +where + F: FnOnce() -> proc_macro::TokenStream + UnwindSafe, +{ + ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() + 1)); + let caught = catch_unwind(f); + let dummy = dummy::cleanup(); + let err_storage = imp::cleanup(); + ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() - 1)); + + let gen_error = || { + if proc_macro_hack { + quote! {{ + macro_rules! proc_macro_call { + () => ( unimplemented!() ) + } + + #(#err_storage)* + #dummy + + unimplemented!() + }} + } else { + quote!( #(#err_storage)* #dummy ) + } + }; + + match caught { + Ok(ts) => { + if err_storage.is_empty() { + ts + } else { + gen_error().into() + } + } + + Err(boxed) => match boxed.downcast::<AbortNow>() { + Ok(_) => gen_error().into(), + Err(boxed) => resume_unwind(boxed), + }, + } +} + +fn abort_now() -> ! { + check_correctness(); + panic!(AbortNow) +} + +thread_local! { + static ENTERED_ENTRY_POINT: Cell<usize> = Cell::new(0); +} + +struct AbortNow; + +fn check_correctness() { + if ENTERED_ENTRY_POINT.with(|flag| flag.get()) == 0 { + panic!( + "proc-macro-error API cannot be used outside of `entry_point` invocation, \ + perhaps you forgot to annotate your #[proc_macro] function with `#[proc_macro_error]" + ); + } +} + +/// **ALL THE STUFF INSIDE IS NOT PUBLIC API!!!** +#[doc(hidden)] +pub mod __export { + // reexports for use in macros + pub extern crate proc_macro; + pub extern crate proc_macro2; + + use proc_macro2::Span; + use quote::ToTokens; + + use crate::SpanRange; + + // inspired by + // https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md#simple-application + + pub trait SpanAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait Span2AsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait ToTokensAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait SpanRangeAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + impl<T: ToTokens> ToTokensAsSpanRange for &T { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + let mut ts = self.to_token_stream().into_iter(); + let first = ts + .next() + .map(|tt| tt.span()) + .unwrap_or_else(Span::call_site); + let last = ts.last().map(|tt| tt.span()).unwrap_or(first); + SpanRange { first, last } + } + } + + impl Span2AsSpanRange for Span { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + SpanRange { + first: *self, + last: *self, + } + } + } + + impl SpanAsSpanRange for proc_macro::Span { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + SpanRange { + first: Span::call_site(), + last: Span::call_site(), + } + } + } + + impl SpanRangeAsSpanRange for SpanRange { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + *self + } + } +} diff --git a/vendor/proc-macro-error/src/macros.rs b/vendor/proc-macro-error/src/macros.rs new file mode 100644 index 0000000..747b684 --- /dev/null +++ b/vendor/proc-macro-error/src/macros.rs @@ -0,0 +1,288 @@ +// FIXME: this can be greatly simplified via $()? +// as soon as MRSV hits 1.32 + +/// Build [`Diagnostic`](struct.Diagnostic.html) instance from provided arguments. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! diagnostic { + // from alias + ($err:expr) => { $crate::Diagnostic::from($err) }; + + // span, message, help + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+ ; $($rest:tt)+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + let diag = $crate::Diagnostic::spanned_range( + span_range, + $level, + format!($fmt, $($args),*) + ); + $crate::__pme__suggestions!(diag $($rest)*); + diag + }}; + + ($span:expr, $level:expr, $msg:expr ; $($rest:tt)+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + let diag = $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()); + $crate::__pme__suggestions!(diag $($rest)*); + diag + }}; + + // span, message, no help + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + $crate::Diagnostic::spanned_range( + span_range, + $level, + format!($fmt, $($args),*) + ) + }}; + + ($span:expr, $level:expr, $msg:expr) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()) + }}; + + + // trailing commas + + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+, ; $($rest:tt)+) => { + $crate::diagnostic!($span, $level, $fmt, $($args),* ; $($rest)*) + }; + ($span:expr, $level:expr, $msg:expr, ; $($rest:tt)+) => { + $crate::diagnostic!($span, $level, $msg ; $($rest)*) + }; + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+,) => { + $crate::diagnostic!($span, $level, $fmt, $($args),*) + }; + ($span:expr, $level:expr, $msg:expr,) => { + $crate::diagnostic!($span, $level, $msg) + }; + // ($err:expr,) => { $crate::diagnostic!($err) }; +} + +/// Abort proc-macro execution right now and display the error. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +#[macro_export] +macro_rules! abort { + ($err:expr) => { + $crate::diagnostic!($err).abort() + }; + + ($span:expr, $($tts:tt)*) => { + $crate::diagnostic!($span, $crate::Level::Error, $($tts)*).abort() + }; +} + +/// Shortcut for `abort!(Span::call_site(), msg...)`. This macro +/// is still preferable over plain panic, panics are not for error reporting. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! abort_call_site { + ($($tts:tt)*) => { + $crate::abort!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }; +} + +/// Emit an error while not aborting the proc-macro right away. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_error { + ($err:expr) => { + $crate::diagnostic!($err).emit() + }; + + ($span:expr, $($tts:tt)*) => {{ + let level = $crate::Level::Error; + $crate::diagnostic!($span, level, $($tts)*).emit() + }}; +} + +/// Shortcut for `emit_error!(Span::call_site(), ...)`. This macro +/// is still preferable over plain panic, panics are not for error reporting.. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_call_site_error { + ($($tts:tt)*) => { + $crate::emit_error!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }; +} + +/// Emit a warning. Warnings are not errors and compilation won't fail because of them. +/// +/// **Does nothing on stable** +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_warning { + ($span:expr, $($tts:tt)*) => { + $crate::diagnostic!($span, $crate::Level::Warning, $($tts)*).emit() + }; +} + +/// Shortcut for `emit_warning!(Span::call_site(), ...)`. +/// +/// **Does nothing on stable** +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_call_site_warning { + ($($tts:tt)*) => {{ + $crate::emit_warning!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __pme__suggestions { + ($var:ident) => (); + + ($var:ident $help:ident =? $msg:expr) => { + let $var = if let Some(msg) = $msg { + $var.suggestion(stringify!($help), msg.to_string()) + } else { + $var + }; + }; + ($var:ident $help:ident =? $span:expr => $msg:expr) => { + let $var = if let Some(msg) = $msg { + $var.span_suggestion($span.into(), stringify!($help), msg.to_string()) + } else { + $var + }; + }; + + ($var:ident $help:ident =? $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help =? $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident =? $span:expr => $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help =? $span => $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + + + ($var:ident $help:ident = $msg:expr) => { + let $var = $var.suggestion(stringify!($help), $msg.to_string()); + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+) => { + let $var = $var.suggestion( + stringify!($help), + format!($fmt, $($args),*) + ); + }; + ($var:ident $help:ident = $span:expr => $msg:expr) => { + let $var = $var.span_suggestion($span.into(), stringify!($help), $msg.to_string()); + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+) => { + let $var = $var.span_suggestion( + $span.into(), + stringify!($help), + format!($fmt, $($args),*) + ); + }; + + ($var:ident $help:ident = $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args),*); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $span:expr => $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*); + $crate::__pme__suggestions!($var $($rest)*); + }; + + // trailing commas + + ($var:ident $help:ident = $msg:expr,) => { + $crate::__pme__suggestions!($var $help = $msg) + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+,) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args)*) + }; + ($var:ident $help:ident = $span:expr => $msg:expr,) => { + $crate::__pme__suggestions!($var $help = $span => $msg) + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),*,) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args)*) + }; + ($var:ident $help:ident = $msg:expr, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $msg; $($rest)*) + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args),*; $($rest)*) + }; + ($var:ident $help:ident = $span:expr => $msg:expr, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $msg; $($rest)*) + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*; $($rest)*) + }; +} diff --git a/vendor/proc-macro-error/src/sealed.rs b/vendor/proc-macro-error/src/sealed.rs new file mode 100644 index 0000000..a2d5081 --- /dev/null +++ b/vendor/proc-macro-error/src/sealed.rs @@ -0,0 +1,3 @@ +pub trait Sealed {} + +impl Sealed for crate::Diagnostic {} diff --git a/vendor/proc-macro-error/tests/macro-errors.rs b/vendor/proc-macro-error/tests/macro-errors.rs new file mode 100644 index 0000000..dd60f88 --- /dev/null +++ b/vendor/proc-macro-error/tests/macro-errors.rs @@ -0,0 +1,8 @@ +extern crate trybuild; + +#[cfg_attr(skip_ui_tests, ignore)] +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/vendor/proc-macro-error/tests/ok.rs b/vendor/proc-macro-error/tests/ok.rs new file mode 100644 index 0000000..cf64c02 --- /dev/null +++ b/vendor/proc-macro-error/tests/ok.rs @@ -0,0 +1,10 @@ +extern crate test_crate; + +use test_crate::*; + +ok!(it_works); + +#[test] +fn check_it_works() { + it_works(); +} diff --git a/vendor/proc-macro-error/tests/runtime-errors.rs b/vendor/proc-macro-error/tests/runtime-errors.rs new file mode 100644 index 0000000..13108a2 --- /dev/null +++ b/vendor/proc-macro-error/tests/runtime-errors.rs @@ -0,0 +1,13 @@ +use proc_macro_error::*; + +#[test] +#[should_panic = "proc-macro-error API cannot be used outside of"] +fn missing_attr_emit() { + emit_call_site_error!("You won't see me"); +} + +#[test] +#[should_panic = "proc-macro-error API cannot be used outside of"] +fn missing_attr_abort() { + abort_call_site!("You won't see me"); +} diff --git a/vendor/proc-macro-error/tests/ui/abort.rs b/vendor/proc-macro-error/tests/ui/abort.rs new file mode 100644 index 0000000..f631182 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/abort.rs @@ -0,0 +1,11 @@ +extern crate test_crate; +use test_crate::*; + +abort_from!(one, two); +abort_to_string!(one, two); +abort_format!(one, two); +direct_abort!(one, two); +abort_notes!(one, two); +abort_call_site_test!(one, two); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/abort.stderr b/vendor/proc-macro-error/tests/ui/abort.stderr new file mode 100644 index 0000000..c5399d9 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/abort.stderr @@ -0,0 +1,48 @@ +error: abort!(span, from) test + --> $DIR/abort.rs:4:13 + | +4 | abort_from!(one, two); + | ^^^ + +error: abort!(span, single_expr) test + --> $DIR/abort.rs:5:18 + | +5 | abort_to_string!(one, two); + | ^^^ + +error: abort!(span, expr1, expr2) test + --> $DIR/abort.rs:6:15 + | +6 | abort_format!(one, two); + | ^^^ + +error: Diagnostic::abort() test + --> $DIR/abort.rs:7:15 + | +7 | direct_abort!(one, two); + | ^^^ + +error: This is an error + + = note: simple note + = help: simple help + = help: simple hint + = note: simple yay + = note: format note + = note: Some note + = note: spanned simple note + = note: spanned format note + = note: Some note + + --> $DIR/abort.rs:8:14 + | +8 | abort_notes!(one, two); + | ^^^ + +error: abort_call_site! test + --> $DIR/abort.rs:9:1 + | +9 | abort_call_site_test!(one, two); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/vendor/proc-macro-error/tests/ui/append_dummy.rs b/vendor/proc-macro-error/tests/ui/append_dummy.rs new file mode 100644 index 0000000..53d6fea --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/append_dummy.rs @@ -0,0 +1,13 @@ +extern crate test_crate; +use test_crate::*; + +enum NeedDefault { + A, + B +} + +append_dummy!(need_default); + +fn main() { + let _ = NeedDefault::default(); +} diff --git a/vendor/proc-macro-error/tests/ui/append_dummy.stderr b/vendor/proc-macro-error/tests/ui/append_dummy.stderr new file mode 100644 index 0000000..8a47dda --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/append_dummy.stderr @@ -0,0 +1,5 @@ +error: append_dummy test + --> $DIR/append_dummy.rs:9:15 + | +9 | append_dummy!(need_default); + | ^^^^^^^^^^^^ diff --git a/vendor/proc-macro-error/tests/ui/children_messages.rs b/vendor/proc-macro-error/tests/ui/children_messages.rs new file mode 100644 index 0000000..fb9e6dc --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/children_messages.rs @@ -0,0 +1,6 @@ +extern crate test_crate; +use test_crate::*; + +children_messages!(one, two, three, four); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/children_messages.stderr b/vendor/proc-macro-error/tests/ui/children_messages.stderr new file mode 100644 index 0000000..3b49d83 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/children_messages.stderr @@ -0,0 +1,23 @@ +error: main macro message + --> $DIR/children_messages.rs:4:20 + | +4 | children_messages!(one, two, three, four); + | ^^^ + +error: child message + --> $DIR/children_messages.rs:4:25 + | +4 | children_messages!(one, two, three, four); + | ^^^ + +error: main syn::Error + --> $DIR/children_messages.rs:4:30 + | +4 | children_messages!(one, two, three, four); + | ^^^^^ + +error: child syn::Error + --> $DIR/children_messages.rs:4:37 + | +4 | children_messages!(one, two, three, four); + | ^^^^ diff --git a/vendor/proc-macro-error/tests/ui/dummy.rs b/vendor/proc-macro-error/tests/ui/dummy.rs new file mode 100644 index 0000000..caa4827 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/dummy.rs @@ -0,0 +1,13 @@ +extern crate test_crate; +use test_crate::*; + +enum NeedDefault { + A, + B +} + +dummy!(need_default); + +fn main() { + let _ = NeedDefault::default(); +} diff --git a/vendor/proc-macro-error/tests/ui/dummy.stderr b/vendor/proc-macro-error/tests/ui/dummy.stderr new file mode 100644 index 0000000..bae078a --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/dummy.stderr @@ -0,0 +1,5 @@ +error: set_dummy test + --> $DIR/dummy.rs:9:8 + | +9 | dummy!(need_default); + | ^^^^^^^^^^^^ diff --git a/vendor/proc-macro-error/tests/ui/emit.rs b/vendor/proc-macro-error/tests/ui/emit.rs new file mode 100644 index 0000000..c5c7db0 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/emit.rs @@ -0,0 +1,7 @@ +extern crate test_crate; +use test_crate::*; + +emit!(one, two, three, four, five); +emit_notes!(one, two); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/emit.stderr b/vendor/proc-macro-error/tests/ui/emit.stderr new file mode 100644 index 0000000..9484bd6 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/emit.stderr @@ -0,0 +1,48 @@ +error: emit!(span, from) test + --> $DIR/emit.rs:4:7 + | +4 | emit!(one, two, three, four, five); + | ^^^ + +error: emit!(span, expr1, expr2) test + --> $DIR/emit.rs:4:12 + | +4 | emit!(one, two, three, four, five); + | ^^^ + +error: emit!(span, single_expr) test + --> $DIR/emit.rs:4:17 + | +4 | emit!(one, two, three, four, five); + | ^^^^^ + +error: Diagnostic::emit() test + --> $DIR/emit.rs:4:24 + | +4 | emit!(one, two, three, four, five); + | ^^^^ + +error: emit_call_site_error!(expr) test + --> $DIR/emit.rs:4:1 + | +4 | emit!(one, two, three, four, five); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: This is an error + + = note: simple note + = help: simple help + = help: simple hint + = note: simple yay + = note: format note + = note: Some note + = note: spanned simple note + = note: spanned format note + = note: Some note + + --> $DIR/emit.rs:5:13 + | +5 | emit_notes!(one, two); + | ^^^ diff --git a/vendor/proc-macro-error/tests/ui/explicit_span_range.rs b/vendor/proc-macro-error/tests/ui/explicit_span_range.rs new file mode 100644 index 0000000..82bbebc --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/explicit_span_range.rs @@ -0,0 +1,6 @@ +extern crate test_crate; +use test_crate::*; + +explicit_span_range!(one, two, three, four); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/explicit_span_range.stderr b/vendor/proc-macro-error/tests/ui/explicit_span_range.stderr new file mode 100644 index 0000000..781a71e --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/explicit_span_range.stderr @@ -0,0 +1,5 @@ +error: explicit SpanRange + --> $DIR/explicit_span_range.rs:4:22 + | +4 | explicit_span_range!(one, two, three, four); + | ^^^^^^^^^^^^^^^ diff --git a/vendor/proc-macro-error/tests/ui/misuse.rs b/vendor/proc-macro-error/tests/ui/misuse.rs new file mode 100644 index 0000000..e6d2d24 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/misuse.rs @@ -0,0 +1,11 @@ +extern crate proc_macro_error; +use proc_macro_error::abort; + +struct Foo; + +#[allow(unused)] +fn foo() { + abort!(Foo, "BOOM"); +} + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/misuse.stderr b/vendor/proc-macro-error/tests/ui/misuse.stderr new file mode 100644 index 0000000..8eaf645 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/misuse.stderr @@ -0,0 +1,13 @@ +error[E0599]: no method named `FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange` found for reference `&Foo` in the current scope + --> $DIR/misuse.rs:8:5 + | +4 | struct Foo; + | ----------- doesn't satisfy `Foo: quote::to_tokens::ToTokens` +... +8 | abort!(Foo, "BOOM"); + | ^^^^^^^^^^^^^^^^^^^^ method not found in `&Foo` + | + = note: the method `FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange` exists but the following trait bounds were not satisfied: + `Foo: quote::to_tokens::ToTokens` + which is required by `&Foo: proc_macro_error::__export::ToTokensAsSpanRange` + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/vendor/proc-macro-error/tests/ui/multiple_tokens.rs b/vendor/proc-macro-error/tests/ui/multiple_tokens.rs new file mode 100644 index 0000000..215928f --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/multiple_tokens.rs @@ -0,0 +1,6 @@ +extern crate test_crate; + +#[test_crate::multiple_tokens] +type T = (); + +fn main() {} \ No newline at end of file diff --git a/vendor/proc-macro-error/tests/ui/multiple_tokens.stderr b/vendor/proc-macro-error/tests/ui/multiple_tokens.stderr new file mode 100644 index 0000000..c6172c6 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/multiple_tokens.stderr @@ -0,0 +1,5 @@ +error: ... + --> $DIR/multiple_tokens.rs:4:1 + | +4 | type T = (); + | ^^^^^^^^^^^^ diff --git a/vendor/proc-macro-error/tests/ui/not_proc_macro.rs b/vendor/proc-macro-error/tests/ui/not_proc_macro.rs new file mode 100644 index 0000000..e241c5c --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/not_proc_macro.rs @@ -0,0 +1,4 @@ +use proc_macro_error::proc_macro_error; + +#[proc_macro_error] +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/not_proc_macro.stderr b/vendor/proc-macro-error/tests/ui/not_proc_macro.stderr new file mode 100644 index 0000000..f19f01b --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/not_proc_macro.stderr @@ -0,0 +1,10 @@ +error: #[proc_macro_error] attribute can be used only with procedural macros + + = hint: if you are really sure that #[proc_macro_error] should be applied to this exact function, use #[proc_macro_error(allow_not_macro)] + + --> $DIR/not_proc_macro.rs:3:1 + | +3 | #[proc_macro_error] + | ^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/vendor/proc-macro-error/tests/ui/option_ext.rs b/vendor/proc-macro-error/tests/ui/option_ext.rs new file mode 100644 index 0000000..dfbfc03 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/option_ext.rs @@ -0,0 +1,6 @@ +extern crate test_crate; +use test_crate::*; + +option_ext!(one, two); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/option_ext.stderr b/vendor/proc-macro-error/tests/ui/option_ext.stderr new file mode 100644 index 0000000..91b151e --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/option_ext.stderr @@ -0,0 +1,7 @@ +error: Option::expect_or_abort() test + --> $DIR/option_ext.rs:4:1 + | +4 | option_ext!(one, two); + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/vendor/proc-macro-error/tests/ui/proc_macro_hack.rs b/vendor/proc-macro-error/tests/ui/proc_macro_hack.rs new file mode 100644 index 0000000..2504bdd --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/proc_macro_hack.rs @@ -0,0 +1,10 @@ +// Adapted from https://github.com/dtolnay/proc-macro-hack/blob/master/example/src/main.rs +// Licensed under either of Apache License, Version 2.0 or MIT license at your option. + +use proc_macro_hack_test::add_one; + +fn main() { + let two = 2; + let nine = add_one!(two) + add_one!(2 + 3); + println!("nine = {}", nine); +} diff --git a/vendor/proc-macro-error/tests/ui/proc_macro_hack.stderr b/vendor/proc-macro-error/tests/ui/proc_macro_hack.stderr new file mode 100644 index 0000000..0e984f9 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/proc_macro_hack.stderr @@ -0,0 +1,26 @@ +error: BOOM + --> $DIR/proc_macro_hack.rs:8:25 + | +8 | let nine = add_one!(two) + add_one!(2 + 3); + | ^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: BOOM + --> $DIR/proc_macro_hack.rs:8:41 + | +8 | let nine = add_one!(two) + add_one!(2 + 3); + | ^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +warning: unreachable expression + --> $DIR/proc_macro_hack.rs:8:32 + | +8 | let nine = add_one!(two) + add_one!(2 + 3); + | ------------- ^^^^^^^^^^^^^^^ unreachable expression + | | + | any code following this expression is unreachable + | + = note: `#[warn(unreachable_code)]` on by default + = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/vendor/proc-macro-error/tests/ui/result_ext.rs b/vendor/proc-macro-error/tests/ui/result_ext.rs new file mode 100644 index 0000000..bdd560d --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/result_ext.rs @@ -0,0 +1,7 @@ +extern crate test_crate; +use test_crate::*; + +result_unwrap_or_abort!(one, two); +result_expect_or_abort!(one, two); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/result_ext.stderr b/vendor/proc-macro-error/tests/ui/result_ext.stderr new file mode 100644 index 0000000..f2dc0e4 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/result_ext.stderr @@ -0,0 +1,11 @@ +error: Result::unwrap_or_abort() test + --> $DIR/result_ext.rs:4:25 + | +4 | result_unwrap_or_abort!(one, two); + | ^^^ + +error: BOOM: Result::expect_or_abort() test + --> $DIR/result_ext.rs:5:25 + | +5 | result_expect_or_abort!(one, two); + | ^^^ diff --git a/vendor/proc-macro-error/tests/ui/to_tokens_span.rs b/vendor/proc-macro-error/tests/ui/to_tokens_span.rs new file mode 100644 index 0000000..a7c3fc9 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/to_tokens_span.rs @@ -0,0 +1,6 @@ +extern crate test_crate; +use test_crate::*; + +to_tokens_span!(std::option::Option); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/to_tokens_span.stderr b/vendor/proc-macro-error/tests/ui/to_tokens_span.stderr new file mode 100644 index 0000000..b8c4968 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/to_tokens_span.stderr @@ -0,0 +1,11 @@ +error: whole type + --> $DIR/to_tokens_span.rs:4:17 + | +4 | to_tokens_span!(std::option::Option); + | ^^^^^^^^^^^^^^^^^^^ + +error: explicit .span() + --> $DIR/to_tokens_span.rs:4:17 + | +4 | to_tokens_span!(std::option::Option); + | ^^^ diff --git a/vendor/proc-macro-error/tests/ui/unknown_setting.rs b/vendor/proc-macro-error/tests/ui/unknown_setting.rs new file mode 100644 index 0000000..d8e58ea --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/unknown_setting.rs @@ -0,0 +1,4 @@ +use proc_macro_error::proc_macro_error; + +#[proc_macro_error(allow_not_macro, assert_unwind_safe, trololo)] +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/unknown_setting.stderr b/vendor/proc-macro-error/tests/ui/unknown_setting.stderr new file mode 100644 index 0000000..a55de0b --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/unknown_setting.stderr @@ -0,0 +1,5 @@ +error: unknown setting `trololo`, expected one of `assert_unwind_safe`, `allow_not_macro`, `proc_macro_hack` + --> $DIR/unknown_setting.rs:3:57 + | +3 | #[proc_macro_error(allow_not_macro, assert_unwind_safe, trololo)] + | ^^^^^^^ diff --git a/vendor/proc-macro-error/tests/ui/unrelated_panic.rs b/vendor/proc-macro-error/tests/ui/unrelated_panic.rs new file mode 100644 index 0000000..c74e3e0 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/unrelated_panic.rs @@ -0,0 +1,6 @@ +extern crate test_crate; +use test_crate::*; + +unrelated_panic!(); + +fn main() {} diff --git a/vendor/proc-macro-error/tests/ui/unrelated_panic.stderr b/vendor/proc-macro-error/tests/ui/unrelated_panic.stderr new file mode 100644 index 0000000..d46d689 --- /dev/null +++ b/vendor/proc-macro-error/tests/ui/unrelated_panic.stderr @@ -0,0 +1,7 @@ +error: proc macro panicked + --> $DIR/unrelated_panic.rs:4:1 + | +4 | unrelated_panic!(); + | ^^^^^^^^^^^^^^^^^^^ + | + = help: message: unrelated panic test