consensus: BIP66 strict-DER signatures (full backport) — activate at 1,055,555 #33

Closed
opened 2026-06-15 23:12:59 +00:00 by dobbscoin · 1 comment
dobbscoin commented 2026-06-15 23:12:59 +00:00

Revised 2026-06-16: activation height changed from 1,025,000 → 1,055,555 to comply with the feature-freeze policy in #20 (no consensus merges to main between h=998,000 and h=1,050,667). The mid-OFFSIG argument has been replaced with a post-freeze + bundle-with-#6 argument. See § Why activate at 1,055,555.

Goal

Bring OFF's signature-encoding rules to consensus-level strict-DER (BIP66). Replace the existing pre-0.10-era lenient canonical-encoding helper with the self-contained strict-DER parser used by Bitcoin Core 0.11+ that doesn't depend on OpenSSL version quirks. Enforce at both mempool and block validation from fork height onward.

Flagged by @9019x on 2026-06-14 as the gating issue for OFF being taken seriously by exchanges and wallets — exchange-integration checklists literally grep source for BIP66 / DERSIG. He has stated he's stepping back from contribution until this lands.

Problem

OFF carries the pre-BIP66 lineage from Bitcoin Core 0.10:

  • src/script.h:42 — SCRIPT_VERIFY_STRICTENC flag exists.
  • src/script.cpp:252-305 — IsCanonicalSignature() is the manual canonical check that pre-dates BIP66.
  • src/main.cpp:968 — mempool (AcceptToMemoryPool) already passes STRICTENC.
  • src/main.cpp:2120-2121 — block validation (ConnectBlock) does NOT pass STRICTENC.

Two structural issues:

  1. Enforcement gap. A non-strict-DER signature smuggled into a block (e.g., by a miner who patched their daemon to skip mempool relay) is currently accepted into the chain. The mempool rejects what blocks don't.

  2. OpenSSL-version drift risk. The existing IsCanonicalSignature() performs manual structural checks but the actual ECDSA verify path still passes bytes to OpenSSL. OpenSSL's lenient handling of edge-case DER encodings has shifted across versions in the past and could shift again — if it does, OFF's consensus rules drift with it. BIP66's distinguishing contribution was the self-contained strict-DER parser that removed OpenSSL from consensus.

For OFF to credibly claim BIP66 compliance against an exchange-integration audit, both gaps need to close.

What BIP66 actually requires

From https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki:

A signature is valid only if:

  • It is at most 73 bytes long.
  • Its byte layout matches the strict DER ECDSA signature format: 30 <total-len> 02 <r-len> <r> 02 <s-len> <s> <sighash-byte>.
  • <total-len> equals signature length minus 3.
  • <r-len> is 1 to 33; if <r> is 33 bytes the leading byte must be 0x00 and the next byte must have its high bit set.
  • <s-len> follows the same rules as <r-len>.
  • <r> and <s> are non-negative when interpreted as 2's-complement signed (i.e., their high byte's high bit is unset) and not excessively padded.
  • <sighash-byte> is one of 0x01, 0x02, 0x03, 0x81, 0x82, 0x83.

The empty signature (used as OP_CHECKMULTISIG padding to signal "no signature") is still allowed.

Constants

Constant Value Rationale
SCRIPT_VERIFY_DERSIG (1U << 4) New flag, distinct from SCRIPT_VERIFY_STRICTENC. Allows mixed enforcement during transition.
HARDFORK_DERSIG_MAIN_OFF 1,055,555 Same height as COINBASE_MATURITY (#32) and BIP65 CLTV (#34); bundled with #6 rolling-checkpoints. 4,889 blocks past freeze-end (~3.4 days).
HARDFORK_DERSIG_TESTNET_OFF 100 Trivial; mirrors LWMA-3 testnet activation.

Why activate at 1,055,555 (post-freeze, bundled with #6)

Original draft placed activation at h=1,025,000 inside the OFFSIG window, on the "Conclave-only mining → zero split risk" rationale. Revised 2026-06-16 to honor the feature-freeze policy in #20, which prohibits merging consensus changes to main between h=998,000 and h=1,050,667. Activation moves to the post-freeze slot.

Why 1,055,555 specifically:

  • Bundles with #6 (rolling checkpoints) at the same height — one upgrade cycle, one BCT post, one Conclave deploy.
  • 4,889 blocks (~3.4 days at 60s) past freeze-end at h=1,050,667. Announce at freeze-end with a 3-day countdown; permissionless miners returning at 1,050,667 have lead time to pull the new binary before the rule trips.
  • Repeats the 5s numerology already used by #6.

Trade vs the original mid-OFFSIG activation: outsider miners can in principle produce blocks between h=1,050,667 and h=1,055,555 under the old rule. The freeze-end upgrade-coordination announcement closes that gap. A 3.4-day lead time is short but standard for post-freeze coordination on a small chain.

Files touched

  • src/script.h — add SCRIPT_VERIFY_DERSIG flag.
  • src/script.cpp — port CheckSignatureEncoding() strict-DER parser from Bitcoin Core 0.11+ (file script/interpreter.cpp in upstream). ~80 LoC, self-contained, no OpenSSL dependency. Plug it into OP_CHECKSIG and OP_CHECKMULTISIG paths next to the existing IsCanonicalSignature() call (the legacy check stays in place so we can run both during transition).
  • src/main.cpp — height-gated flag computation in ConnectBlock line 2120-2121: flags |= (pindex->nHeight >= HARDFORK_DERSIG_MAIN_OFF ? SCRIPT_VERIFY_DERSIG : 0). Same gate added to AcceptToMemoryPool flags line 968.
  • src/pow.h — HARDFORK_DERSIG_MAIN_OFF / _TESTNET_OFF constants next to the LWMA-3 fork heights.

Approx 150-200 LoC total, dominated by the strict-DER parser.

Semantics

The new rule applies to any script verification at nSpendHeight ≥ HARDFORK_DERSIG_MAIN_OFF. Pre-fork txs and the historical chain replay/reindex through the legacy (lenient) path — no risk of invalidating buried blocks.

Practically, libsecp256k1 and OpenSSL-since-2015 already produce strict-DER signatures, so the user-visible breakage surface is near zero. The change targets the smuggle-non-canonical-into-block attack vector, not legitimate signing.

Test plan

  1. Regtest: extend qa/rpc-tests/ with three scenarios:
    • Pre-fork: mine a block with a hand-crafted non-strict-DER signature; expect accept.
    • Post-fork: same hand-crafted sig; expect bad-txns-nonstandard-inputs or equivalent reject.
    • Boundary: tx with non-strict-DER sig in mempool at fork-1; reject at fork+1 even if same UTXO.
  2. Strict-DER parser unit tests: port the Bitcoin Core 0.11 script_tests.json test vectors covering BIP66 edge cases (excessive padding, missing sighash byte, oversized <r>, etc.).
  3. Testnet activation: per @9019x's process note, ship v2.0.x-rc-bipsoft with testnet fork at h=100 first. Mine a hand-crafted non-strict-DER tx pre-fork and post-fork; confirm behavior.
  4. Replay test: full reindex from genesis on a v2.0.x node. Confirm no historical blocks invalidate.

Risks / mitigations

Risk Mitigation
Historical block contains non-strict-DER sig and reindex fails New rule is gated on nSpendHeight ≥ fork. Pre-fork blocks revalidate with legacy lenient path.
User-built wallets producing non-strict-DER sigs None known in practice (libsecp256k1 / OpenSSL 1.0.2+ both produce strict-DER). Custom signers should be flagged on BCT announcement.
Old binary mining a non-strict-DER sig into a block between freeze-end and activation Rule only trips at h=1,055,555. ~3.4-day announce-and-countdown at freeze-end (h=1,050,667) gives returning permissionless miners lead time to pull the new binary.
OpenSSL upgrade changes ECDSA verify behavior post-fork New parser is self-contained. Verify path still uses OpenSSL's ECDSA_verify for the actual EC math, but encoding validation is now OFF's own code.
Bundle with COINBASE_MATURITY (#32) and BIP65 CLTV (#34) at h=1,055,555 Same activation height; one upgrade cycle. Independent rule sets, distinct flag bits. Each revertable independently if testnet exposes a problem.
Bundle interaction with BIP65 CLTV (#34) Same fork height, independent rule sets. CLTV uses SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY flag, distinct from SCRIPT_VERIFY_DERSIG.

Process

Bundle into v2.0.x-rc-bipsoft with COINBASE_MATURITY (#32) and BIP65 CLTV (#34). All three rules share HARDFORK_*_MAIN_OFF = 1,055,555. Branch development on feat/v2.0.x-rc-bipsoft; do not merge to main until freeze-end (post h=1,050,667). Cut tag with testnet activation at h=100; live-test on testnet for ~3 days; promote to mainnet activation tag once green.

Community chat for live discussion: https://23skidoo.info/discord

Activation milestone (to add to WHERE_WE_LEFT_OFF.md)

BIP66 strict-DER: h=1,055,555 (bundled in v2.0.x-rc-bipsoft)

References

  • BIP66: https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki
  • Bitcoin Core PR adding the strict-DER parser: bitcoin/bitcoin#5713 (Pieter Wuille, "Make signature encoding rules less permissive")
  • src/script.cpp:252-305 — existing legacy IsCanonicalSignature()
  • src/main.cpp:968 — mempool currently passes STRICTENC
  • src/main.cpp:2120-2121 — block validation currently does NOT pass STRICTENC
  • src/main.cpp:1934-1941 — the "for now" hedge comment from 2014
  • Issue #6 — rolling checkpoints at 1,055,555 (bundled at same height)
  • Issue #20 — feature-freeze policy h=998,000 → h=1,050,667 (the constraint that forced this revision)
  • Issue #32 — COINBASE_MATURITY (bundled at same height)
  • Issue #38 — public OFF testnet (live-test prerequisite)
> **Revised 2026-06-16:** activation height changed from 1,025,000 → 1,055,555 to comply with the feature-freeze policy in #20 (no consensus merges to `main` between h=998,000 and h=1,050,667). The mid-OFFSIG argument has been replaced with a post-freeze + bundle-with-#6 argument. See § Why activate at 1,055,555. ## Goal Bring OFF's signature-encoding rules to consensus-level strict-DER (BIP66). Replace the existing pre-0.10-era lenient canonical-encoding helper with the self-contained strict-DER parser used by Bitcoin Core 0.11+ that doesn't depend on OpenSSL version quirks. Enforce at both mempool and block validation from fork height onward. Flagged by @9019x on 2026-06-14 as the gating issue for OFF being taken seriously by exchanges and wallets — exchange-integration checklists literally grep source for `BIP66` / `DERSIG`. He has stated he's stepping back from contribution until this lands. ## Problem OFF carries the pre-BIP66 lineage from Bitcoin Core 0.10: - `src/script.h:42` — `SCRIPT_VERIFY_STRICTENC` flag exists. - `src/script.cpp:252-305` — `IsCanonicalSignature()` is the manual canonical check that pre-dates BIP66. - `src/main.cpp:968` — mempool (`AcceptToMemoryPool`) already passes `STRICTENC`. - `src/main.cpp:2120-2121` — **block validation (`ConnectBlock`) does NOT pass `STRICTENC`.** Two structural issues: 1. **Enforcement gap.** A non-strict-DER signature smuggled into a block (e.g., by a miner who patched their daemon to skip mempool relay) is currently accepted into the chain. The mempool rejects what blocks don't. 2. **OpenSSL-version drift risk.** The existing `IsCanonicalSignature()` performs manual structural checks but the actual ECDSA verify path still passes bytes to OpenSSL. OpenSSL's lenient handling of edge-case DER encodings has shifted across versions in the past and could shift again — if it does, OFF's consensus rules drift with it. BIP66's distinguishing contribution was the self-contained strict-DER parser that removed OpenSSL from consensus. For OFF to credibly claim BIP66 compliance against an exchange-integration audit, both gaps need to close. ## What BIP66 actually requires From https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki: A signature is valid only if: - It is at most 73 bytes long. - Its byte layout matches the strict DER ECDSA signature format: `30 <total-len> 02 <r-len> <r> 02 <s-len> <s> <sighash-byte>`. - `<total-len>` equals signature length minus 3. - `<r-len>` is 1 to 33; if `<r>` is 33 bytes the leading byte must be `0x00` and the next byte must have its high bit set. - `<s-len>` follows the same rules as `<r-len>`. - `<r>` and `<s>` are non-negative when interpreted as 2's-complement signed (i.e., their high byte's high bit is unset) and not excessively padded. - `<sighash-byte>` is one of `0x01`, `0x02`, `0x03`, `0x81`, `0x82`, `0x83`. The empty signature (used as `OP_CHECKMULTISIG` padding to signal "no signature") is still allowed. ## Constants | Constant | Value | Rationale | |---|---|---| | `SCRIPT_VERIFY_DERSIG` | (1U << 4) | New flag, distinct from `SCRIPT_VERIFY_STRICTENC`. Allows mixed enforcement during transition. | | `HARDFORK_DERSIG_MAIN_OFF` | **1,055,555** | Same height as COINBASE_MATURITY (#32) and BIP65 CLTV (#34); bundled with #6 rolling-checkpoints. 4,889 blocks past freeze-end (~3.4 days). | | `HARDFORK_DERSIG_TESTNET_OFF` | 100 | Trivial; mirrors LWMA-3 testnet activation. | ## Why activate at 1,055,555 (post-freeze, bundled with #6) **Original draft** placed activation at h=1,025,000 inside the OFFSIG window, on the "Conclave-only mining → zero split risk" rationale. **Revised 2026-06-16** to honor the feature-freeze policy in #20, which prohibits merging consensus changes to `main` between h=998,000 and h=1,050,667. Activation moves to the post-freeze slot. Why 1,055,555 specifically: - Bundles with #6 (rolling checkpoints) at the same height — one upgrade cycle, one BCT post, one Conclave deploy. - 4,889 blocks (~3.4 days at 60s) past freeze-end at h=1,050,667. Announce at freeze-end with a 3-day countdown; permissionless miners returning at 1,050,667 have lead time to pull the new binary before the rule trips. - Repeats the 5s numerology already used by #6. Trade vs the original mid-OFFSIG activation: outsider miners can in principle produce blocks between h=1,050,667 and h=1,055,555 under the old rule. The freeze-end upgrade-coordination announcement closes that gap. A 3.4-day lead time is short but standard for post-freeze coordination on a small chain. ## Files touched - `src/script.h` — add `SCRIPT_VERIFY_DERSIG` flag. - `src/script.cpp` — port `CheckSignatureEncoding()` strict-DER parser from Bitcoin Core 0.11+ (file `script/interpreter.cpp` in upstream). ~80 LoC, self-contained, no OpenSSL dependency. Plug it into `OP_CHECKSIG` and `OP_CHECKMULTISIG` paths next to the existing `IsCanonicalSignature()` call (the legacy check stays in place so we can run both during transition). - `src/main.cpp` — height-gated flag computation in `ConnectBlock` line 2120-2121: `flags |= (pindex->nHeight >= HARDFORK_DERSIG_MAIN_OFF ? SCRIPT_VERIFY_DERSIG : 0)`. Same gate added to `AcceptToMemoryPool` flags line 968. - `src/pow.h` — `HARDFORK_DERSIG_MAIN_OFF` / `_TESTNET_OFF` constants next to the LWMA-3 fork heights. Approx 150-200 LoC total, dominated by the strict-DER parser. ## Semantics The new rule applies to any **script verification at** `nSpendHeight ≥ HARDFORK_DERSIG_MAIN_OFF`. Pre-fork txs and the historical chain replay/reindex through the legacy (lenient) path — no risk of invalidating buried blocks. Practically, libsecp256k1 and OpenSSL-since-2015 already produce strict-DER signatures, so the user-visible breakage surface is near zero. The change targets the smuggle-non-canonical-into-block attack vector, not legitimate signing. ## Test plan 1. **Regtest**: extend `qa/rpc-tests/` with three scenarios: - Pre-fork: mine a block with a hand-crafted non-strict-DER signature; expect accept. - Post-fork: same hand-crafted sig; expect `bad-txns-nonstandard-inputs` or equivalent reject. - Boundary: tx with non-strict-DER sig in mempool at fork-1; reject at fork+1 even if same UTXO. 2. **Strict-DER parser unit tests**: port the Bitcoin Core 0.11 `script_tests.json` test vectors covering BIP66 edge cases (excessive padding, missing sighash byte, oversized `<r>`, etc.). 3. **Testnet activation**: per @9019x's process note, ship `v2.0.x-rc-bipsoft` with testnet fork at h=100 first. Mine a hand-crafted non-strict-DER tx pre-fork and post-fork; confirm behavior. 4. **Replay test**: full reindex from genesis on a v2.0.x node. Confirm no historical blocks invalidate. ## Risks / mitigations | Risk | Mitigation | |---|---| | Historical block contains non-strict-DER sig and reindex fails | New rule is gated on `nSpendHeight ≥ fork`. Pre-fork blocks revalidate with legacy lenient path. | | User-built wallets producing non-strict-DER sigs | None known in practice (libsecp256k1 / OpenSSL 1.0.2+ both produce strict-DER). Custom signers should be flagged on BCT announcement. | | Old binary mining a non-strict-DER sig into a block between freeze-end and activation | Rule only trips at h=1,055,555. ~3.4-day announce-and-countdown at freeze-end (h=1,050,667) gives returning permissionless miners lead time to pull the new binary. | | OpenSSL upgrade changes ECDSA verify behavior post-fork | New parser is self-contained. Verify path still uses OpenSSL's `ECDSA_verify` for the actual EC math, but encoding validation is now OFF's own code. | | Bundle with COINBASE_MATURITY (#32) and BIP65 CLTV (#34) at h=1,055,555 | Same activation height; one upgrade cycle. Independent rule sets, distinct flag bits. Each revertable independently if testnet exposes a problem. | | Bundle interaction with BIP65 CLTV (#34) | Same fork height, independent rule sets. CLTV uses `SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY` flag, distinct from `SCRIPT_VERIFY_DERSIG`. | ## Process Bundle into `v2.0.x-rc-bipsoft` with COINBASE_MATURITY (#32) and BIP65 CLTV (#34). All three rules share `HARDFORK_*_MAIN_OFF = 1,055,555`. Branch development on `feat/v2.0.x-rc-bipsoft`; do not merge to `main` until freeze-end (post h=1,050,667). Cut tag with testnet activation at h=100; live-test on testnet for ~3 days; promote to mainnet activation tag once green. Community chat for live discussion: https://23skidoo.info/discord ## Activation milestone (to add to WHERE_WE_LEFT_OFF.md) `BIP66 strict-DER: h=1,055,555 (bundled in v2.0.x-rc-bipsoft)` ## References - BIP66: https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki - Bitcoin Core PR adding the strict-DER parser: bitcoin/bitcoin#5713 (Pieter Wuille, "Make signature encoding rules less permissive") - `src/script.cpp:252-305` — existing legacy `IsCanonicalSignature()` - `src/main.cpp:968` — mempool currently passes `STRICTENC` - `src/main.cpp:2120-2121` — block validation currently does NOT pass `STRICTENC` - `src/main.cpp:1934-1941` — the "for now" hedge comment from 2014 - Issue #6 — rolling checkpoints at 1,055,555 (bundled at same height) - Issue #20 — feature-freeze policy h=998,000 → h=1,050,667 (the constraint that forced this revision) - Issue #32 — COINBASE_MATURITY (bundled at same height) - Issue #38 — public OFF testnet (live-test prerequisite)
dobbscoin commented 2026-07-27 21:50:42 +00:00

Live on mainnet. Activated on schedule at h=1,055,555 (2026-07-23), block 00000000438cf73291060c7c2caeadd568b7e432c192f4f44b3d84eafb157c7c.

Strict-DER signature encoding is now enforced on every transaction — the malleability class BIP66 targets is closed on OFF. ~5,800 blocks validated under the rule with no forks and no rejected transactions observed in the wild; the mixed-version soak (v2.0.8.7 / v2.0.9 / v2.1.0-rc) remains fork-free at delta 0.

Shipped in v2.0.9-Eldersign. This also unblocks the [post-#33] gate on #47 (OpenSSL evacuation), tracked separately. Closing.

Live on mainnet. Activated on schedule at h=1,055,555 (2026-07-23), block `00000000438cf73291060c7c2caeadd568b7e432c192f4f44b3d84eafb157c7c`. Strict-DER signature encoding is now enforced on every transaction — the malleability class BIP66 targets is closed on OFF. ~5,800 blocks validated under the rule with no forks and no rejected transactions observed in the wild; the mixed-version soak (v2.0.8.7 / v2.0.9 / v2.1.0-rc) remains fork-free at delta 0. Shipped in v2.0.9-Eldersign. This also unblocks the `[post-#33]` gate on #47 (OpenSSL evacuation), tracked separately. Closing.
dobbscoin closed this issue 2026-07-27 21:50:43 +00:00
Sign in to join this conversation.
No labels
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
SubGeniusFinance/Offerings-to-Cthulhu#33
No description provided.