Skip to content

Commit b8ae2bc

Browse files
committed
feat: add TypeScript SDK, PQ-EVM opcodes, contract examples, fix all compiler warnings
TypeScript SDK (@qnet/sdk): - Add QNetClient with full HTTP API coverage (blocks, txs, balances, contracts, faucet) - Add ContractHandle for PQ-EVM contract interaction (deploy, call, send) - Add address utilities: EON format, publicKeyHashToAddress, formatQNC/parseQNC - Add QNetSubscription for real-time block/tx polling-based subscriptions - Add polling helpers: pollBlocks, waitForHeight, waitForTransaction - Add structured error hierarchy: QNetApiError, QNetTransactionError, QNetAddressError - Add Jest test suite for client methods - Add build pipeline: rollup (CJS + ESM + .d.ts), tsconfig, package.json PQ-EVM (pq_evm.rs): - Switch Dilithium from ML-DSA-87 (level 5) to ML-DSA-65 (level 3) to align with quantum_crypto.rs - Implement full EVM opcode subset: arithmetic (SUB/DIV/MOD/ADDMOD/MULMOD/EXP), comparison/bitwise (LT/GT/EQ/ISZERO/AND/OR/XOR/NOT/BYTE/SHL/SHR), hashing (KECCAK256), stack ops (POP/DUP1-3/SWAP1-2), memory (MLOAD/MSTORE/MSTORE8), storage (SLOAD/SSTORE), control flow (JUMP/JUMPI/JUMPDEST), environment opcodes, PUSH1-8, LOG0-1 - Replace stub PQ_SIGN with real dilithium3 sign() writing signature to memory - Replace stub PQ_VERIFY with real dilithium3 open() returning 1/0 - Replace stub PQ_ENCRYPT with real kyber1024 encapsulate() writing ciphertext to memory - Remove PQ_DECRYPT stub (KEM decap belongs at account layer, not contract layer) - Fix deploy_standard_contract: replace missing include_bytes with minimal init-code Contract examples (qnet-contracts/examples/): - Add qnet_token.rs: QNC-compatible fungible token (ERC-20 analogue for PQ-EVM) - Add node_stake_registry.rs: stake registry with Dilithium-authenticated node entries - Add pq_multisig.rs: m-of-n multisig wallet using post-quantum signatures - Add qnc_yield_pool.rs: yield pool with epoch-based reward distribution - Add README.md documenting contract examples and PQ-EVM usage Compiler warnings (qnet-integration): 318 to 0 - Remove blanket allow(unused_imports/variables/dead_code/unused_mut) from lib.rs - Remove all unused imports across 17 files (sha3, aes_gcm, chacha20poly1305, flate2, HashMap, DashMap, Mutex, rayon, bincode, chrono, once_cell, various qnet_state types) - Remove unused constants: HEALTH_CHECK_INTERVAL_SECS, MAX_CACHE_SIZE, MAX_PARALLEL_TX - Fix ~60 unused variables: prefix _ on params and locals across node.rs, unified_p2p.rs, rpc.rs, activation_validation.rs, storage.rs, reward_sharding.rs, quantum_crypto.rs - Remove unnecessary mut qualifiers in parallel_executor.rs, rpc.rs, storage.rs - Add targeted allow(dead_code) on ~50 specific items (protocol methods kept for future use, struct fields in serialized types, serde helper modules) - Mark tests module with cfg(test) so it only compiles during cargo test - Fix base64::engine::general_purpose import in unified_p2p.rs functions Made-with: Cursor
1 parent 6cad853 commit b8ae2bc

40 files changed

Lines changed: 3964 additions & 563 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# QNet Native Smart Contract Examples
2+
3+
Examples of contracts targeting QNet's Post-Quantum EVM (PQ-EVM).
4+
All signing uses **CRYSTALS-Dilithium3** (ML-DSA-65, NIST FIPS 204 Level 3) — the same algorithm
5+
used by QNet's core consensus layer (`quantum_crypto.rs`).
6+
Encryption uses **CRYSTALS-Kyber1024** (NIST FIPS 203).
7+
8+
---
9+
10+
## Contracts
11+
12+
| File | Description |
13+
|------|-------------|
14+
| `qnet_token.rs` | QEP-20 fungible token — QNet equivalent of ERC-20 |
15+
| `pq_multisig.rs` | 2-of-N PQ multi-sig wallet using Dilithium3 signatures (ML-DSA-65) |
16+
| `qnc_yield_pool.rs` | **User/wallet-facing** QNC yield pool — purely financial, no node logic. Any wallet user stakes QNC and earns proportional yield (`reward = pool × stake / total_staked`). Deployer funds the reward pool; QNet block production is unaffected. |
17+
18+
---
19+
20+
## Compiling & Deploying
21+
22+
> Requires the `qnet-node` binary built in release mode.
23+
24+
```bash
25+
# 1. Build the node binary (run from repo root)
26+
cargo build --release --bin qnet-node
27+
28+
# 2. Deploy a contract via the RPC endpoint
29+
curl -X POST http://localhost:9876/api/v1/contract/deploy \
30+
-H "Content-Type: application/json" \
31+
-d '{
32+
"from": "YOUR_WALLET_ADDRESS",
33+
"bytecode": "0x6000F3",
34+
"gas_limit": 1000000,
35+
"value": 0,
36+
"pq_signature": "BASE64_DILITHIUM_SIG"
37+
}'
38+
39+
# 3. Call a deployed contract
40+
curl -X POST http://localhost:9876/api/v1/contract/call \
41+
-H "Content-Type: application/json" \
42+
-d '{
43+
"to": "CONTRACT_ADDRESS",
44+
"data": "0x00000001...",
45+
"gas_limit": 100000
46+
}'
47+
```
48+
49+
---
50+
51+
## Writing Your Own Contract
52+
53+
QNet contracts are currently written as Rust modules that produce bytecode
54+
via the `PQEvmInterpreter`. A high-level source language (`qnet-sol`) is on
55+
the roadmap. For now, use the helper functions in `qnet_token.rs` as a template.
56+
57+
Key differences from Ethereum Solidity:
58+
59+
| Feature | Ethereum | QNet |
60+
|---------|----------|------|
61+
| Signature scheme | ECDSA (secp256k1) | Dilithium3 / ML-DSA-65 (NIST FIPS 204 L3) |
62+
| Hash function | Keccak-256 | Keccak-256 + SHA3-256 |
63+
| Encryption | none native | Kyber1024 via PQ_ENCRYPT opcode |
64+
| Block time | ~12 s | ~1 s (microblock) |
65+
| Finality | ~2 min (32 conf.) | MacroBlock (~90 s) |
66+
| Custom opcodes || `0xE0` MICROBLOCK_COMMIT, `0xE1` MICROBLOCK_VERIFY |
67+
| PQ opcodes || `0xF0` PQ_SIGN, `0xF1` PQ_VERIFY, `0xF2` PQ_ENCRYPT |
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/// Post-Quantum Multi-Signature Wallet Contract
2+
///
3+
/// A 2-of-N Dilithium5 multisig wallet deployed on QNet's PQ-EVM.
4+
/// Unlike ECDSA multisigs (Gnosis Safe, etc.), all signing keys use
5+
/// CRYSTALS-Dilithium5 — quantum-resistant per NIST FIPS 204.
6+
///
7+
/// # Protocol
8+
///
9+
/// 1. Deploy with list of owner PQ public keys and required threshold.
10+
/// 2. Any owner calls `submit_tx(to, value, data)` → returns `tx_id`.
11+
/// 3. Each owner calls `confirm(tx_id)` with a valid Dilithium5 signature.
12+
/// 4. Once `confirmations >= threshold`, anyone can call `execute(tx_id)`.
13+
///
14+
/// # Storage layout
15+
///
16+
/// | Slot | Content |
17+
/// |------------------------|------------------------------------|
18+
/// | 0x00 | threshold (u64) |
19+
/// | 0x01 | owner_count (u64) |
20+
/// | 0x10_0000 + i | owners[i] public key offset |
21+
/// | 0x20_0000 + tx_id | Transaction { to, value, data_len, executed } |
22+
/// | 0x30_0000 + tx_id * N | confirmations bitmask |
23+
24+
use crate::{Address, PQEvmInterpreter, ExecutionContext, GasConfig};
25+
// QNet consensus uses CRYSTALS-Dilithium3 (ML-DSA-65, NIST FIPS 204 level 3)
26+
use pqcrypto_mldsa::mldsa65 as dilithium3;
27+
use pqcrypto_traits::sign::PublicKey;
28+
29+
// ─────────────────────────────────────────────────────────────────────────────
30+
// Off-chain helper: build a confirm-transaction calldata payload
31+
// ─────────────────────────────────────────────────────────────────────────────
32+
33+
/// Build the calldata bytes for a `confirm(tx_id)` call, including the
34+
/// Dilithium3 signature over the canonical message `"CONFIRM:<tx_id>"`.
35+
///
36+
/// # Arguments
37+
/// * `tx_id` — transaction index in the multisig queue
38+
/// * `secret_key` — caller's Dilithium3 secret key (ML-DSA-65)
39+
///
40+
/// # Returns
41+
/// Raw calldata bytes ready to pass as `input_data` in a QNet transaction.
42+
pub fn build_confirm_calldata(tx_id: u64, secret_key: &dilithium3::SecretKey) -> Vec<u8> {
43+
let canonical_msg = format!("CONFIRM:{}", tx_id);
44+
let signed = dilithium3::sign(canonical_msg.as_bytes(), secret_key);
45+
let sig_bytes = signed.as_bytes();
46+
47+
// Calldata layout:
48+
// [0..4] selector = 0x00_00_00_03 (confirm)
49+
// [4..12] tx_id as big-endian u64
50+
// [12..16] sig_len as big-endian u32
51+
// [16..] signature bytes
52+
let mut data = vec![0x00, 0x00, 0x00, 0x03]; // selector: confirm
53+
data.extend_from_slice(&tx_id.to_be_bytes());
54+
data.extend_from_slice(&(sig_bytes.len() as u32).to_be_bytes());
55+
data.extend_from_slice(sig_bytes);
56+
data
57+
}
58+
59+
/// Build calldata for `submit_tx(to, value, data)`.
60+
pub fn build_submit_calldata(to: Address, value: u64, tx_data: &[u8]) -> Vec<u8> {
61+
// selector: 0x00_00_00_01
62+
let mut data = vec![0x00, 0x00, 0x00, 0x01];
63+
data.extend_from_slice(&to); // 20 bytes
64+
data.extend_from_slice(&value.to_be_bytes()); // 8 bytes
65+
data.extend_from_slice(&(tx_data.len() as u32).to_be_bytes()); // 4 bytes
66+
data.extend_from_slice(tx_data);
67+
data
68+
}
69+
70+
// ─────────────────────────────────────────────────────────────────────────────
71+
// On-chain verification helper (runs inside PQ-EVM via Rust FFI)
72+
// ─────────────────────────────────────────────────────────────────────────────
73+
74+
/// Verify that `sig_bytes` is a valid Dilithium3 signature over
75+
/// `"CONFIRM:<tx_id>"` by the owner whose public key is `pk_bytes`.
76+
///
77+
/// Uses ML-DSA-65 (CRYSTALS-Dilithium3) — the same algorithm that QNet's
78+
/// `quantum_crypto.rs` uses for all consensus signatures.
79+
///
80+
/// Called from the `confirm()` handler inside the PQ-EVM interpreter via the
81+
/// PQ_VERIFY (0xF1) opcode; this Rust function is the underlying implementation.
82+
pub fn verify_confirm_sig(tx_id: u64, pk_bytes: &[u8], sig_bytes: &[u8]) -> bool {
83+
let canonical_msg = format!("CONFIRM:{}", tx_id);
84+
85+
let pk = match dilithium3::PublicKey::from_bytes(pk_bytes) {
86+
Ok(k) => k,
87+
Err(_) => return false,
88+
};
89+
let signed_msg = match dilithium3::SignedMessage::from_bytes(sig_bytes) {
90+
Ok(s) => s,
91+
Err(_) => return false,
92+
};
93+
match dilithium3::open(&signed_msg, &pk) {
94+
Ok(verified_msg) => verified_msg == canonical_msg.as_bytes(),
95+
Err(_) => false,
96+
}
97+
}
98+
99+
/// Pending transaction entry stored in EVM state.
100+
#[derive(Debug, Clone)]
101+
pub struct PendingTx {
102+
pub to: Address,
103+
pub value: u64,
104+
pub data: Vec<u8>,
105+
pub confirmations: Vec<usize>, // confirmed owner indices
106+
pub executed: bool,
107+
}
108+
109+
/// In-memory multisig state (mirrors on-chain SSTORE slots for testing).
110+
pub struct PQMultisig {
111+
pub owners: Vec<dilithium3::PublicKey>,
112+
pub threshold: usize,
113+
pub txs: Vec<PendingTx>,
114+
}
115+
116+
impl PQMultisig {
117+
pub fn new(owners: Vec<dilithium3::PublicKey>, threshold: usize) -> Self {
118+
assert!(threshold <= owners.len(), "threshold > owner count");
119+
Self { owners, threshold, txs: Vec::new() }
120+
}
121+
122+
/// Submit a new transaction, returns tx_id.
123+
pub fn submit_tx(&mut self, to: Address, value: u64, data: Vec<u8>) -> usize {
124+
self.txs.push(PendingTx { to, value, data, confirmations: Vec::new(), executed: false });
125+
self.txs.len() - 1
126+
}
127+
128+
/// Confirm a transaction with a Dilithium5 signature.
129+
/// Returns `true` if the signature is valid and owner hasn't confirmed yet.
130+
pub fn confirm(&mut self, tx_id: usize, owner_idx: usize, sig_bytes: &[u8]) -> bool {
131+
if tx_id >= self.txs.len() || owner_idx >= self.owners.len() { return false; }
132+
if self.txs[tx_id].executed { return false; }
133+
if self.txs[tx_id].confirmations.contains(&owner_idx) { return false; }
134+
135+
if verify_confirm_sig(tx_id as u64, self.owners[owner_idx].as_bytes(), sig_bytes) {
136+
self.txs[tx_id].confirmations.push(owner_idx);
137+
true
138+
} else {
139+
false
140+
}
141+
}
142+
143+
/// Execute if threshold reached. Returns the calldata that would be sent.
144+
pub fn execute(&mut self, tx_id: usize) -> Result<Vec<u8>, &'static str> {
145+
let tx = self.txs.get_mut(tx_id).ok_or("tx not found")?;
146+
if tx.executed { return Err("already executed"); }
147+
if tx.confirmations.len() < self.threshold { return Err("insufficient confirmations"); }
148+
tx.executed = true;
149+
Ok(tx.data.clone())
150+
}
151+
}
152+
153+
#[cfg(test)]
154+
mod tests {
155+
use super::*;
156+
157+
#[test]
158+
fn test_pq_multisig_2of3() {
159+
let (pk1, sk1) = dilithium3::keypair();
160+
let (pk2, sk2) = dilithium3::keypair();
161+
let (pk3, _sk3) = dilithium3::keypair();
162+
163+
let mut wallet = PQMultisig::new(vec![pk1, pk2, pk3], 2);
164+
let to: Address = [0xAA; 20];
165+
let tx_id = wallet.submit_tx(to, 1000, vec![]);
166+
167+
let sig1 = dilithium3::sign(format!("CONFIRM:{}", tx_id).as_bytes(), &sk1);
168+
let sig2 = dilithium3::sign(format!("CONFIRM:{}", tx_id).as_bytes(), &sk2);
169+
170+
assert!(wallet.confirm(tx_id, 0, sig1.as_bytes()));
171+
assert!(wallet.confirm(tx_id, 1, sig2.as_bytes()));
172+
assert!(wallet.execute(tx_id).is_ok());
173+
}
174+
}

0 commit comments

Comments
 (0)