Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kernel/codec/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export class BlockEngine {
i === 0 ? NULL_TXID : this.codec.wtxid(tx));
const root = hexToBytes(this.codec.merkleRoot(wtxids)).reverse();
const reserved = hexToBytes(block.transactions[0].witness?.[0]?.[0] ?? '00'.repeat(32));
if (reserved.length !== 32) return null; // malformed reserved value can never match the commitment
const cat = new Uint8Array(64);
cat.set(root); cat.set(reserved, 32);
return bytesToHex(dsha256(cat));
Expand Down
43 changes: 41 additions & 2 deletions packages/kernel/codec/codec.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,25 @@ class Writer {
export class Codec {
constructor(...schemas) {
this.types = new Map();
// Network proof-of-work ceiling, as a BigInt. Null until chain params are
// supplied; checkProofOfWork skips only the network-specific bound while
// it is unset, never the universal range checks.
this.powLimit = null;
for (const schema of schemas) {
for (const node of schema['@graph'] ?? []) {
if (node['@type'] === 'ConsensusStruct') this.types.set(shortId(node['@id']), node);
}
}
}

// Supply the network parameters the consensus checks need. Called by
// createKernel and by HeaderEngine.fromSchemas; safe to call directly when
// constructing a Codec by hand.
setChainParams(params) {
if (params?.powLimit != null) this.powLimit = BigInt('0x' + params.powLimit);
return this;
}

def(typeName) {
const def = this.types.get(typeName);
if (!def) throw new Error(`unknown type: ${typeName}`);
Expand Down Expand Up @@ -275,7 +287,34 @@ export class Codec {
return exponent <= 3 ? mantissa >> (8n * BigInt(3 - exponent)) : mantissa << (8n * BigInt(exponent - 3));
}

checkProofOfWork(header) {
return BigInt('0x' + this.blockHash(header)) <= this.expandCompact(header.bits);
// The target plus the two flags Bitcoin Core's arith_uint256::SetCompact
// reports. `negative` is the sign bit the mantissa mask discards; `overflow`
// marks an nBits whose target cannot fit in 256 bits. Both make a header
// invalid regardless of network.
expandCompactChecked(bits) {
const size = bits >>> 24;
const word = bits & 0x007fffff;
return {
target: this.expandCompact(bits),
negative: word !== 0 && (bits & 0x00800000) !== 0,
overflow: word !== 0 && (size > 34
|| (word > 0xff && size > 33)
|| (word > 0xffff && size > 32)),
};
}

// Core's CheckProofOfWork: the target must be in range *before* the hash is
// compared against it. Without the range checks any nBits claiming an easier
// target than the network permits is accepted, which makes a proof of work
// free to forge.
//
// `powLimit` defaults to the value set on this codec (see `setChainParams`);
// when neither is available the network-specific bound cannot be applied, but
// the negative, zero and overflow checks always are.
checkProofOfWork(header, powLimit = this.powLimit) {
const { target, negative, overflow } = this.expandCompactChecked(header.bits);
if (negative || overflow || target === 0n) return false;
if (powLimit != null && target > powLimit) return false;
return BigInt('0x' + this.blockHash(header)) <= target;
}
}
3 changes: 3 additions & 0 deletions packages/kernel/codec/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export class HeaderEngine {
static fromSchemas(codec, chainSchema, validateSchema, network = 'btc:mainnet') {
const params = chainSchema['@graph'].find((n) => n['@id'] === network);
const ruleSet = validateSchema['@graph'].find((n) => n['@type'] === 'RuleSet' && n.phase === 'header');
// The SPV ruleset has no difficulty rule, so btc:rule-spv-pow is the only
// work check there; the codec needs the ceiling to enforce it.
codec.setChainParams(params);
return new HeaderEngine(codec, params, ruleSet);
}

Expand Down
9 changes: 5 additions & 4 deletions packages/kernel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ export * from './codec/hash.js';
export * from './codec/secp256k1.js';
export const schemas = { core, proof, script, chain, validate };

export function createKernel() {
export function createKernel(network = 'btc:mainnet') {
const codec = new Codec(core, proof);
const scriptEngine = ScriptEngine.fromSchemas(script, chain);
codec.setChainParams(chain['@graph'].find((n) => n['@id'] === network));
const scriptEngine = ScriptEngine.fromSchemas(script, chain, network);
const limits = script['@graph'].find((n) => n['@id'] === 'btc:scriptLimits');
const interpreter = new ScriptInterpreter(codec, scriptEngine, limits);
const headers = HeaderEngine.fromSchemas(codec, chain, validate);
const blocks = BlockEngine.fromSchemas(codec, chain, validate, script);
const headers = HeaderEngine.fromSchemas(codec, chain, validate, network);
const blocks = BlockEngine.fromSchemas(codec, chain, validate, script, network);
const spv = SpvEngine.fromSchemas(codec, validate);
return { codec, script: scriptEngine, interpreter, headers, blocks, spv, schemas };
}
Expand Down
25 changes: 18 additions & 7 deletions tools/sync-kernel.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// step: re-run this, review the diff, commit. The engine's canonical dev home
// stays bitcoin-desktop/schema; this is a vendored copy.
//
// PIN: bitcoin-desktop/schema @ b213128 (Fix #70 OP_CODESEPARATOR, PR #71)
// PIN: bitcoin-desktop/schema @ PENDING (PoW target range checks + witness
// reserved-value guard, PR #72) — update to the merge SHA before landing.
//
// Sources (override via env for non-default checkouts):
// SCHEMA_DIR — the schema repo (codec/ + schema/*.jsonld)
Expand Down Expand Up @@ -32,8 +33,17 @@ for (const k of SCHEMAS) {
// wasm: copy, and make the loader fetch the wasm co-located with the MODULE
// (import.meta.url) so it works served from any URL / the CDN.
await copyFile(new URL('secp256k1.wasm', WASM), new URL('wasm/secp256k1.wasm', OUT));
const loader = (await readFile(new URL('secp-wasm.js', WASM), 'utf8'))
.replace(/loadSecpWasm\(url = [^)]*\)/, "loadSecpWasm(url = new URL('./secp256k1.wasm', import.meta.url).href)");
const WASM_DEFAULT_URL = "loadSecpWasm(url = new URL('./secp256k1.wasm', import.meta.url).href)";
let loader = await readFile(new URL('secp-wasm.js', WASM), 'utf8');
// Idempotent: the replacement text contains a ')', so [^)]* matches this
// rewrite's own output and would corrupt it on a second pass. That happens
// whenever WASM_DIR points at a previously generated copy.
if (!loader.includes(WASM_DEFAULT_URL)) {
if (!/loadSecpWasm\(url = [^)]*\)/.test(loader)) {
throw new Error('secp-wasm.js: no loadSecpWasm(url = ...) default to rewrite');
}
loader = loader.replace(/loadSecpWasm\(url = [^)]*\)/, WASM_DEFAULT_URL);
}
await writeFile(new URL('wasm/secp-wasm.js', OUT), loader);

// barrel + package.json
Expand All @@ -56,13 +66,14 @@ export * from './codec/hash.js';
export * from './codec/secp256k1.js';
export const schemas = { core, proof, script, chain, validate };

export function createKernel() {
export function createKernel(network = 'btc:mainnet') {
const codec = new Codec(core, proof);
const scriptEngine = ScriptEngine.fromSchemas(script, chain);
codec.setChainParams(chain['@graph'].find((n) => n['@id'] === network));
const scriptEngine = ScriptEngine.fromSchemas(script, chain, network);
const limits = script['@graph'].find((n) => n['@id'] === 'btc:scriptLimits');
const interpreter = new ScriptInterpreter(codec, scriptEngine, limits);
const headers = HeaderEngine.fromSchemas(codec, chain, validate);
const blocks = BlockEngine.fromSchemas(codec, chain, validate, script);
const headers = HeaderEngine.fromSchemas(codec, chain, validate, network);
const blocks = BlockEngine.fromSchemas(codec, chain, validate, script, network);
const spv = SpvEngine.fromSchemas(codec, validate);
return { codec, script: scriptEngine, interpreter, headers, blocks, spv, schemas };
}
Expand Down