Skip to content

The signature assert family - #41

Merged
EvanWinget merged 6 commits into
mainfrom
signature-asserts
Aug 9, 2026
Merged

EvanWinget merged 6 commits into
mainfrom
signature-asserts

Conversation

@EvanWinget

Copy link
Copy Markdown
Owner

The signature assert family: eight conditions at 0x10 to 0x17, ASSERT_SIG_MY_TXID, ASSERT_SIG_MY_SCRIPTPUBKEY, ASSERT_SIG_MY_AMOUNT, the three two-field combinations, ASSERT_SIG_RAW, and ASSERT_SIG_MY_OUTPOINT. Each carries (pubkey, message, signature) and asserts BIP340 Schnorr verification over a per-variant tagged-hash digest of the carrying input's own prevout data and the program's message. New validation rule 8, stage 5 occupied, decision 23 with divergences C16 to C19.

Spec authority: spec/CONDITIONS.md signature assert family (encoding, digest table, operands), spec/VALIDATION.md rule 8 (the fact, idempotence, guidance), the amended stages preamble, and three new invariants. Design ratified in chat 2026-08-09, recorded as decision 23 in docs/condition-record.md.

Review guide

Read the commits in order:

  1. spec (334f41e): the family section and rule 8. The digest construction is the heart: tagged_hash(per-variant tag, fixed-length binding fields, then the message). Check the variant table against the Chia mapping (0x10+k for Chia 43+k) and the stages amendment.
  2. docs (dad8e05): decision 23 is the full design record including the annex decline, the digest-shape decision you ratified on trust, the MEV analysis, and the seal sequencing. C16 to C19 are the divergence rows. The C17 injectivity wart (an AGG_SIG_AMOUNT signature for amount 128 replays as amount 0 in deployed Chia) is the evidence for the tagged-hash divergence.
  3. validation (32e665f): one parse function, one digest function, one check function, verification ordered last.
  4. tools (30f58c2): the pinned JSON form.
  5. vectors (abb0582): 55 new cases. Start with the two footgun cases (footgun_intended_effects, footgun_rewritten_effects_accepted): both are valid-true on purpose, pinning that a signature over a fixed message does NOT protect a spend's effects. This is the cost of the program-composed digest decision you ratified on my recommendation, and these vectors plus rule 8's guidance paragraph are the compensating control we agreed on. If anything in this PR deserves your skepticism, it is this pair.
  6. tests (d489bfc): the spec's variant table restated as a literal and asserted against the implementation, plus five hypothesis properties.

Verify independently:

.venv/bin/pytest python/tests -q                      # 159 passed
.venv/bin/python tools/run_vectors.py                 # 28 files, 884 cases, 0 failures
.venv/bin/python tools/diff_clvm.py --count 10000 --seed 827   # clean (VM untouched)
ci/lint/lint.sh

Five-agent Fable review, run pre-PR, all catches folded in

  • Spec lens (1 major, 4 smaller): the check-order sentence a vector pinned unsaid is now in the family's Arguments paragraph, the family block row renamed, the stages amendment trimmed to behavior only, marker placement and verification wording tightened.
  • Implementation lens (clean, 1 nit): a 5,376-point digest grid against an independent construction and 1,088 signer round trips found zero mismatches. The nit (a bare else in the binding dispatch that would silently emit outpoint bytes on a future table typo) is fixed with an explicit branch and a loud failure.
  • Adversarial lens (clean): 400,000 crafted digest-absorption attempts with zero cross-variant collisions, replay in all directions, BIP340 edge keys, parse seam, cross-family acceptance interactions, and the footgun boundary confirmed to be exactly the pinned one.
  • Coverage lens (2 majors): fifteen mutants run, two survived the original corpus and both are now killed deterministically. The serious one: my first off-curve pubkey (0x01 bytes) actually lifts to a real curve point, so a mutant treating lift failure as success, an accept-invalid class, passed 874 cases. The vectors now carry the BIP340 official off-curve x plus an above-field-prime companion. The second: a check-only-the-first-3-conditions mutant survived because no rejection sat deep in a list, killed by the ninth-position failing triple. Also fixed: the invariant suite previously imported the variant table from the implementation, weakening its independence claim, and now pins a literal copy.
  • Docs lens (4 majors, all documentation accuracy): two stale comparison-doc paragraphs rewritten, failing vectors added for the three combination variants so register row 8 states the truth, the execution plan's 2026-08-06 digest-shape bullet amended so a cold reader is not pointed at the declined design, and C17 corrected on ME's verbatim-challenge suffix.

Declined finding, flagged for you: the docs lens noted decision 11's historical text still says stage 5 collects "(pk, message) pairs". Left per the earlier-entries-keep-their-wording convention, with the current contract stated in decision 23 and the glossary row. Say the word if you want an amendment note under decision 11 instead.

Confidence: 90 percent

What supports it: the digest construction is triple-checked (implementation, an independent hand-transcribed probe grid, and vector signatures generated from a third restatement), the verification relation is bound to the official BIP340 vectors through the shared secp_verify implementation, the adversarial pass found nothing, and every identified mutant now dies deterministically.

The residual 10 percent, named: the digest construction is novel consensus surface with no external oracle (no deployed system to diff against, the ground-rule-4 situation, mitigated by invariants and the adversarial corpus rather than eliminated), the footgun boundary is a designed-in non-protection whose safety depends on Phase 3 tooling defaults that do not exist yet, and cross-rule error precedence remains unpinned by design (decision 22), which is recorded freedom rather than risk but keeps one degree of implementation variance alive.

Eight conditions at 0x10 to 0x17, offset continuity 0x10+k with
Chia's AGG_SIG opcodes 43+k in Chia's order. Each is an assert
carrying (pubkey, message, signature) and is satisfied exactly when
the signature verifies under BIP340 over a tagged-hash digest of
the binding data and the message. Validation rule 8 checks the
family at stage 5, and the stages intro now states stage 5's real
character: it adds no context beyond stage 2.

Design ratified in chat 2026-08-09 (decision 23 in
docs/condition-record.md, landing in the docs commit): BIP340
x-only Schnorr with the signature as third operand, program-composed
messages with the delegated-program idiom as guidance, tagged-hash
domain separation replacing Chia's suffix construction, the full
eight-variant binding menu under the self-assert MY naming
convention, ASSERT_SIG_RAW adopted as the attestation-import mode.
The MY names resolve the creating-txid vs spending-txid ambiguity
Evan caught in spec review.

Five-agent review responses folded in: the family states its
argument check order (arity first, then operands in order, the
message-family precedent, a vector pinned it unsaid), the family
block table row renamed signature asserts, the stages amendment
trimmed to behavior (the expense rationale lives in the decision
record), the bad_condition_arg marker covers all three operands,
and verification names BIP340's Verify directly.

Chia parity: opcode continuity, two-operand shape extended by the
signature operand, the 1024-byte message cap, and per-variant
domain separation. Divergences C16 to C19 in the docs commit:
per-triple secp verification replaces aggregated BLS, tagged-hash
prefix replaces the suffix construction, the AGG_SIG_UNSAFE suffix
firewall is unnecessary by construction, and duplicate triples are
idempotent facts rather than counted aggregate terms.
The signature assert family's decision record: scheme and carrier
(BIP340 triples, the annex and external-section declines), the
program-composed digest shape with the delegated-program idiom and
its pre-registered reserved-tier alternative, tagged-hash domain
separation with the probe-found injectivity wart in Chia's suffix
construction as evidence, the full binding menu under the MY naming
convention with the decision 18 precedent applied twice, the RAW
adoption on batching-complement grounds, the composition and MEV
analysis, and the two-PR sequencing that spawns the seal unit.

Provenance: the 2026-08-09 research pass, source read at the 0.46.0
tag plus probes through validate_clvm_and_signature, the entry
point that runs Chia's real aggregate verification, with all eight
final-message constructions confirmed end to end. The novel-layer
register gains rule 8's row and closes decision 12's obligation.
Glossary rows for the family, RAW, the delegated-program idiom, and
the planned seal pair. Comparison table flipped from planned to
landed, execution plan amended.

Five-agent review responses folded in: two stale comparison
paragraphs rewritten (the open-item sentence and the vocabulary-gap
observation), the execution plan's 2026-08-06 digest-shape bullet
amended (conditions hash declined, triples not pairs), C17 states
ME's verbatim-challenge suffix and UNSAFE's suffixlessness, the
register rows describe exactly what the corpus pins, register row 4
lists the signature duplicates cases, the glossary stage row
matches the amended stages preamble, and the family row's
parenthetical no longer swallows RAW.
Parsing: eight opcodes at 0x10 to 0x17 share one AssertSig shape,
three operands checked in order (pubkey exactly 32 bytes, message at
most 1024, signature exactly 64), arity first, every defect
bad_condition_arg. A shape-legal pubkey that lifts to no curve
point is deliberately not a parse error: BIP340 defines lift
failure as verification failure, and the family error
unsatisfied_sig_assert covers it at stage 5.

Checking: rule 8 verifies each triple against the tagged-hash
digest of its variant, sha256(sha256(tag) twice, then the
fixed-length binding fields, then the message), fields read from
the carrying input's own prevout data. Verification is ordered
last in validate_transaction: it is the costly check and runs only
on transactions every other rule accepts, the stage 5 contract.
The binding-field dispatch names every field and raises on an
unknown one, a review-response hardening so a future table typo
fails loudly instead of silently emitting outpoint bytes.

The digest reuses secp256k1.verify, the same BIP340 relation the
VM's secp_verify runs, so the official BIP340 vectors bind this
path through the shared implementation.

Spec: CONDITIONS.md signature assert family, VALIDATION.md rule 8.
One shape for all eight opcodes, {opcode, pubkey, message,
signature} with the operands hex, since the variant lives in the
opcode byte.
Signatures produced by the vendored Bitcoin Core framework signer,
the recorded secp_verify signing oracle, digests computed from the
spec's construction independently of the implementation.

Parse layer (15 cases): every variant's parsed form, operand shape
and arity errors in the spec's pinned check order, pair-typed
operands rejected in every slot, the off-curve pubkey parsing as
shape-legal (verification owns lift failure), the 1024-byte
boundary, and the 0x18 family gap.

Validation layer (38 cases): a satisfied case per variant and the
eight-variant conjunction, variant separation in five directions
(txid vs outpoint, raw vs bound both ways, and each single-field
variant against the two-field variant extending it), failing cases
for all eight variants, operand byte flips, outpoint index binding
with the high-byte counterexample and the txid mode ignoring the
index, amount binding at zero and above 2^32 with the truncation
counterexample, the shared-tail scriptPubKey twin, sibling-input
isolation, RAW replay acceptance and the no-firewall tag-suffix
acceptance, the decision 23 footgun pinned as what the layer does
not protect, transaction field isolation, and the two-signer
conjunction.

Duplicates (2 cases): identical triples idempotent within an
input, the copied condition diverging across inputs.

Five-agent review responses: the coverage lens ran fifteen mutants
and two survived the original corpus, both now killed
deterministically. The off-curve cases carry the BIP340 official
off-curve x coordinate (the first draft's 0x01-bytes key lifts to
a real curve point, so the lift-failure-as-success mutant, an
accept-invalid bug class, had passed 874 cases) plus an
above-field-prime companion, and the ninth-position failing triple
kills the check-only-the-first-k loop mutant the corpus never
exercised. The s-at-group-order case pins a value-defect rejection
through the composed path.
Six properties over the family: the spec's variant table restated
as a literal and asserted equal to the implementation's (a
review-response hardening, the suite previously imported the
table, so a field-order mutant passed every invariant), environment
independence, a lone triple satisfied exactly when the carrier's
digest fields match the signed coin's (RAW satisfied everywhere),
operand byte-flip metamorphic rejection, variant separation across
all opcode pairs, and rule 4 in-place duplication invariance.

Signatures come from the vendored Core framework signer and
digests from the literal table, so a digest divergence between
spec and implementation fails here. The two coins differ in every
binding field including an index and an amount above one byte and
2^32 respectively, the truncating-compare separators. Example
counts are capped because pure-Python verification dominates each
example and the mutation surface is small and discrete.
@EvanWinget
EvanWinget merged commit 6ad0a51 into main Aug 9, 2026
2 checks passed
@EvanWinget
EvanWinget deleted the signature-asserts branch August 9, 2026 21:22
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