Skip to content

K0 Fix B2: uses_signer contract registry (fixes #90) - #91

Closed
liqdmetal wants to merge 6 commits into
DEROFDN:community-devfrom
liqdmetal:feature/k0-fix-b2-uses-signer
Closed

K0 Fix B2: uses_signer contract registry (fixes #90)#91
liqdmetal wants to merge 6 commits into
DEROFDN:community-devfrom
liqdmetal:feature/k0-fix-b2-uses-signer

Conversation

@liqdmetal

@liqdmetal liqdmetal commented Aug 22, 2026

Copy link
Copy Markdown

Summary

Hard-fork proposal: the consensus half of closing the ringsize-2 loophole for SC calls. A ringsize-2 SC_TX exposes the signer by design (the ring IS sender+receiver; the proof's parity bit identifies the sender). This makes ringsize 2 structurally impossible for contracts that don't need it — without requiring any contract-author changes.

The mechanism

  1. SC_META_DATA gains a NoSigner bit — the high bit of the existing Type byte. The 33-byte wire format is unchanged, so existing metadata stays valid and existing contracts default to uses_signer=true (preserving behavior, exactly as the K0 design specifies).
  2. Auto-detection at install — the parsed contract AST is scanned for SIGNER() calls (ContractUsesSigner, case-insensitive to match the DVM's dispatch). If none are found, the NoSigner bit is set. No contract author changes needed — a contract that never calls SIGNER() is automatically eligible for ringsize ≥ 4.
  3. Consensus enforcement — SC_TX + ringsize 2 + NoSigner-marked contract → rejected with a clear error. Contracts that genuinely call SIGNER() keep ringsize 2 (owner-gated entrypoints) until the verify_sig migration (K0 Fix C) removes that need.
  4. IsPrivate() masks the bit — the private-SC check (meta.Type == 1) would have broken for a private contract that is also NoSigner (0x81 ≠ 1). IsPrivate() masks the B2 bit so private + NoSigner coexist in one Type byte, and InitializePrivate still dispatches correctly.

Why this matters

The ringsize-2 SC call is the last legitimate reason the protocol needs ringsize 2. Once verify_sig (DVM v9, PR #84) lands, owner-gated contracts can authorize at ringsize ≥ 4 via signature-in-payload, and ringsize 2 becomes fully extinct.

Changes

File Change
dvm/sc.go SC_META_DATA NoSigner bit (high bit of Type byte) + ContractUsesSigner AST scan + IsPrivate() mask
blockchain/transaction_execute.go set NoSigner bit at install when no SIGNER() usage detected (height-gated, see below)
blockchain/transaction_verify.go SC_TX + ringsize 2 + NoSigner → reject (reads SC meta tree); SC_INSTALL ring-2 check parses the code in SCDATA
dvm/k0_b2_test.go 7 tests: detection, bit round-trip, 33-byte legacy compat, private+NoSigner coexistence
blockchain/k0_b2_consensus_test.go 4 consensus-rule tests for the SC_INSTALL path

Hardening (wargame findings, now in the code)

  1. SC_INSTALL ring-2 hole (closed). The original check only covered SC_CALL (SCID non-zero). An install of a no-SIGNER contract at ringsize 2 was unchecked — exposing the deployer for no legitimate reason. k0SCInstallRing2Reject now parses the SCDATA code, scans for SIGNER(), and rejects a ring-2 install of a no-SIGNER contract.
  2. Chain-split gating (critical). The NoSigner bit changes the SC_META tree bytes, which are committed into the chain state root (blockchain.go sc_change_cache → tree hash). If a B2 node set the bit (or rejected a ring-2 install) before the fork while a legacy node did not, the two node types would produce different state roots / disagree on block validity → instant chain split. Both the install-path SetNoSigner and the verify-path SC_INSTALL rejection are therefore gated on the hard-fork height (K0_MIN_RING4_HEIGHT on this branch — same activation height as the B1 floor). Pre-fork: byte-identical behavior, zero divergence. Post-fork: all nodes run the same rule. SC_CALL is naturally safe because the bit can only appear post-fork (install is gated), so pre-fork contracts always read NoSigner=false.

Tests

  • TestContractUsesSigner — detects SIGNER() (incl. lowercase), no false positive on SIGNER-free contracts
  • TestSCMetaNoSignerBit — bit set/clear, 33-byte serialization round-trip
  • TestSCMetaPrivateAndNoSignerCoexist — private + NoSigner in one byte
  • TestSCMeta33ByteCompatibility — legacy meta still decodes
  • TestK0SCInstallRing2Reject_* — no-SIGNER install rejected at ring 2, SIGNER contract allowed, no-SCDATA allowed, case-insensitive

Relationship to the K0 package

PR Fix Effect
#80 Fix A wallet warns (non-consensus, ships now)
#82 Fix B1 consensus min-ring-4 for NORMAL/BURN
this Fix B2 consensus: ringsize-2 SC_TX only for contracts that declare SIGNER()
#84 Fix C (DVM v9) verify_sig removes the last ringsize-2 need

Open questions (for review)

  1. Auto-detection vs explicit declaration: AST scanning is automatic but can be fooled by indirect calls (e.g. a function that passes SIGNER() as a parameter — not possible in DVM-BASIC today, but worth stating). An explicit OPTION NOSIGNER pragma could be added later. The scan matches the DVM's own dispatch (case-insensitive, whole-line REM/; comment handling — a Rust reimplementation of the parser was differentially tested against this rule and had a mid-line-comment bug fixed as a result).
  2. HF scheduling + activation-height alignment (RESOLVED). This is a storage-metadata + consensus check change; needs a hard fork. Activation height is aligned with the B1 floor (PR K0 Fix B1: consensus min-ring-4 floor for NORMAL/BURN txs (fixes #81) #82): both gate on K0_MIN_RING4_HEIGHT (mainnet 7,600,000, testnet 0). This branch carries its own K0_MIN_RING4_HEIGHT config field (mainnet 7,600,000 / testnet 0) and both the install-path SetNoSigner and the verify-path SC_INSTALL rejection key off it — so B2's rejection and B1's floor turn on together at one height. (Earlier drafts gated on MAJOR_HF3_HEIGHT 7,504,640, which would have activated B2 ~95k blocks before B1 — that window is closed.)
  3. Differential coverage: the SIGNER-detection rule is pinned Go↔Rust in the derohe-rs harness (dvm_uses_signer, 12 vectors); the floor rule is pinned too (k0_floor, 159 vectors). The NoSigner meta-bit round-trip would benefit from the same treatment when the fork is green-lit.

Branch: feature/k0-fix-b2-uses-signer in the fork liqdmetal/derohe-improvements-by-liqdmetal. Carries the build fixes: re-vendored modules (vendor/modules.txt present) and the KickReader removal.

…T auto-detect)

The consensus half of closing the ringsize-2 loophole for SC calls: a
ringsize-2 SC_TX exposes the signer by design (the ring IS sender+receiver,
parity selects the sender). This makes ringsize 2 structurally impossible
for contracts that don't need it.

Design (k0-fix-design.md Fix B2):
- SC_META_DATA gains a NoSigner bit (high bit of the Type byte). The 33-byte
  wire format is UNCHANGED, so existing metadata stays valid and existing
  contracts default to uses_signer=true (preserving behavior).
- At install (transaction_execute.go), the parsed contract AST is scanned
  for SIGNER() calls (dvm.ContractUsesSigner). No SIGNER() -> NoSigner bit
  set. Contract authors need no changes.
- Consensus enforcement (transaction_verify.go): SC_TX + ringsize 2 +
  NoSigner-marked contract -> rejected with a clear error. Contracts that
  genuinely call SIGNER() keep ringsize 2 (owner-gated entrypoints) until
  the verify_sig migration (K0 Fix C, DVM v9 PR DEROFDN#84) removes that need.

Tests (dvm/k0_b2_test.go):
- TestContractUsesSigner: detects SIGNER(), no false positive
- TestSCMetaNoSignerBit: bit set/clear, 33-byte round-trip, private+NoSigner
  coexistence

Composes with the K0 package: Fix A (DEROFDN#80) warns, Fix B1 (DEROFDN#82) bans
ringsize-2 NORMAL/BURN, Fix B2 bans ringsize-2 SC_TX for NoSigner
contracts, Fix C (DEROFDN#84 verify_sig) removes the last legitimate ringsize-2
need.

Also carries the build-manifest fix (go.mod/go.sum).
@liqdmetal
liqdmetal force-pushed the feature/k0-fix-b2-uses-signer branch from 1fe892b to a909b6b Compare August 23, 2026 03:26
Regenerates vendor/ via go mod tidy + go mod vendor (adds missing
vendor/modules.txt) so the tree builds from a fresh clone without
-mod=mod. Deps unchanged.
community-dev base calls l.Operation.KickReader(), which no published
chzyer/readline implements (v1.5.1) -> wallet-cli fails to compile from a
fresh clone. Replace with the same UI shim used on the main fork branch:
_ = l.Operation (read-unblock helper, wallet-only, not consensus).
…jection

Two gaps closed on top of the B2 NoSigner bit:

1. IsPrivate(): the private-SC check was meta.Type == 1, which breaks for a
   private contract that is also NoSigner (Type == 0x81 != 1) — the
   InitializePrivate entrypoint would never run. IsPrivate() masks the B2
   bit so private + NoSigner coexist in one Type byte.

2. SC_INSTALL at ringsize 2 was unchecked — only SC_CALL read the stored
   NoSigner bit. An install of a no-SIGNER contract at ringsize 2 now
   parses the code in SCDATA and rejects (k0SCInstallRing2Reject), closing
   the deployer-exposure hole.

Adds 4 blockchain consensus-rule tests + 7 dvm tests (detection, bit
round-trip, 33-byte legacy compatibility, private+NoSigner coexistence).
…hardening)

Wargame finding: the B2 NoSigner bit was set unconditionally at install and
the SC_INSTALL ring-2 rejection fired unconditionally in verify. SC_META
bytes feed the chain state root, so a B2 node setting/rejecting differently
than a legacy node pre-fork makes the two node types disagree on block
validity -> INSTANT CHAIN SPLIT.

Fix: both the install-path SetNoSigner and the verify-path SC_INSTALL
rejection are now gated on MAJOR_HF3_HEIGHT (the post-HF3 hard-fork window).
Pre-fork: byte-identical behavior, no divergence. Post-fork: all nodes run
the same rule. SC_CALL is naturally safe because the bit can only appear
post-fork (install gated).
…EIGHT)

The B2 SC rules were gated on MAJOR_HF3_HEIGHT (7,504,640) while the B1
floor (PR DEROFDN#82) activates at K0_MIN_RING4_HEIGHT (7,600,000) — a ~95k-block
window where B2 would reject ring-2 SC installs/calls while ring-2 NORMAL
txs were still legal. Both now share K0_MIN_RING4_HEIGHT (mainnet 7,600,000
post-HF3, testnet 0), so the floor and the SC-signer rules turn on
together at one activation height.
@liqdmetal

Copy link
Copy Markdown
Author

Superseded by PR #121 (the consolidated K0 privacy package). Closing in its favour — same code, but as one reviewable 17-file PR with no vendored-dependency noise.

@liqdmetal liqdmetal closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant