Repository navigation
Conversation
BCHN's IsValidDERSignatureEncoding accepts raw DER signatures from 8 through 72 bytes, inclusive. References: - https://github.com/bitcoin-cash-node/bitcoin-cash-node/blob/b31ed10b4aa4ea54c0a20ed3e536cca05759887c/src/script/sigencoding.cpp - https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki
🦋 Changeset detectedLatest commit: dbd73ba The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
On behalf of Mathieu G. (mr-zwets), written by Claude Opus 5.5: Independent confirmation that this boundary affects consensus, not just error codes. Test done against 8 bytes: changes accept/reject.
With this PR's change applied to a local copy of the 3.1.0-next.8 build, all four accept. The BCH 2026 VMB vectors shipped with BCHN (29,731 vectors, consensus mode) still all pass. 72 bytes: only the error differs. A DER signature can only reach 72 bytes with two 33-byte integers. A 33-byte S is ≥ 2^255 > n/2, so it fails LOW_S, which is a mandatory consensus flag. BCHN then fails with An honest signer with a fixed key cannot produce r < 128 (about 2^-249 per signature). The difference is reachable only with an attacker-chosen key, for example a contract that takes the public key from unlocking data. A libauth-based validator, wallet or indexer then rejects a transaction the network accepts. Reproduction script (Node, only import { createVirtualMachineBch2026, encodeDataPush, flattenBinArray, generateSigningSerializationBch,
hexToBin, secp256k1, sha256 } from '@bitauth/libauth';
const vm = createVirtualMachineBch2026(false); // consensus mode
const b = (...x) => Uint8Array.of(...x);
const cat = (...x) => flattenBinArray(x);
const be32 = (n) => hexToBin(n.toString(16).padStart(64, '0'));
// Public key for which (r, s) is a valid ECDSA signature of `digest` (key recovery; r = 1 is a valid x-coordinate).
const keyFor = (digest, r, s) => secp256k1.recoverPublicKeyCompressed(cat(be32(r), be32(s)), 0, digest);
const der8 = hexToBin('3006020101020101'); // r = 1, s = 1
const der9 = hexToBin('300702010102020080'); // r = 1, s = 0x80 (control)
const p2pkh = b(0x76, 0xa9, 0x14, ...new Uint8Array(20), 0x88, 0xac);
const verify = (locking, makeUnlocking) => {
const sourceOutputs = [{ lockingBytecode: locking, valueSatoshis: 100_000n }];
const transaction = { version: 2, locktime: 0,
inputs: [{ outpointTransactionHash: hexToBin('11'.repeat(32)), outpointIndex: 0, sequenceNumber: 0xffffffff, unlockingBytecode: b() }],
outputs: [{ lockingBytecode: p2pkh, valueSatoshis: 99_000n }] };
transaction.inputs[0].unlockingBytecode = makeUnlocking(transaction, sourceOutputs);
const result = vm.verify({ sourceOutputs, transaction });
return result === true ? 'accept' : `reject (${result})`;
};
const msg = new TextEncoder().encode('oracle message');
for (const [name, der, s] of [['8-byte', der8, 1n], ['9-byte', der9, 0x80n]]) {
// OP_CHECKDATASIG: <pubkey> OP_CHECKDATASIG, unlocked by <sig> <msg>
const q = keyFor(sha256.hash(msg), 1n, s);
console.log(`${name} OP_CHECKDATASIG:`, verify(cat(encodeDataPush(q), b(0xba)), () => cat(encodeDataPush(der), encodeDataPush(msg))));
// OP_CHECKSIG: bare OP_CHECKSIG, unlocked by <sig+ALL|FORKID> <pubkey>
console.log(`${name} OP_CHECKSIG: `, verify(b(0xac), (transaction, sourceOutputs) => {
const ser = generateSigningSerializationBch({ inputIndex: 0, sourceOutputs, transaction },
{ coveredBytecode: b(0xac), signingSerializationType: b(0x41) }, sha256);
const qs = keyFor(sha256.hash(sha256.hash(ser)), 1n, s);
return cat(encodeDataPush(cat(der, b(0x41))), encodeDataPush(qs));
}));
}The same scripts and signature construction, spending real outputs on a BCHN v29.1.0 regtest node: all four pass |
BCHN's IsValidDERSignatureEncoding accepts raw DER signatures from 8 through 72 bytes, inclusive.
References: