From be09284acf833345e5d60c63c459d90874cfb490 Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 23 Aug 2026 19:57:56 -0700 Subject: [PATCH 1/7] tools: the mutation harness tools/mutate.py breaks the reference in one small way at a time and asks whether the vector corpus notices. It generates every mutant of every module in python/bitlisp from a fixed edit set (comparison operators flipped to their neighbor or negation, arithmetic and bitwise operators swapped, and/or exchanged, integer constants moved by one, booleans flipped, if tests negated, not removed, raise deleted, break/continue exchanged), runs the corpus against each mutant in a private mirror of the tree, and reports the survivors with their diffs. With --tests, survivors also run the pytest suite, separating what nothing catches from what the tests catch and the corpus does not. Mirrors copy python/ and tools/ and link vectors/ and puzzles/, so mutants never touch the checkout and parallel workers never see each other's edits. The unmutated tree must pass every oracle the mutants face before any mutant runs, otherwise a broken mirror would report every mutant killed (which is how the first pass's --tests column went wrong before puzzles/ was linked). Timeouts count as kills. The unit test checks that every module yields parseable, distinct mutants, that a constant mutant changes exactly its constant, that the mirror shares data and copies code, and that a broken error-code table dies in the corpus. CLAUDE.md gains the command. --- CLAUDE.md | 1 + ci/lint/codespell-ignore-words.txt | 2 + python/tests/test_mutate.py | 58 ++++ tools/mutate.py | 483 +++++++++++++++++++++++++++++ 4 files changed, 544 insertions(+) create mode 100644 python/tests/test_mutate.py create mode 100644 tools/mutate.py diff --git a/CLAUDE.md b/CLAUDE.md index 3f26017..41ec514 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,6 +154,7 @@ python3 -m venv .venv && .venv/bin/pip install -e ".[dev,oracles]" .venv/bin/bitlisp-compile [source] # v0 language source to bytecode hex .venv/bin/python tools/run_vectors.py # full vector corpus .venv/bin/python tools/diff_clvm.py --count 10000 --seed 1 # diff harness +.venv/bin/python tools/mutate.py --tests # mutation pass, survivors need triage ci/lint/lint.sh # codespell, ruff, whitespace, prose ``` diff --git a/ci/lint/codespell-ignore-words.txt b/ci/lint/codespell-ignore-words.txt index 04e8394..00e80c2 100644 --- a/ci/lint/codespell-ignore-words.txt +++ b/ci/lint/codespell-ignore-words.txt @@ -2,3 +2,5 @@ # above it. # The set-theory property (pairwise disjoint), not "disjointedness". disjointness +# The ast.NotIn comparison operator class, not "not in" misspelled. +NotIn diff --git a/python/tests/test_mutate.py b/python/tests/test_mutate.py new file mode 100644 index 0000000..4335a4f --- /dev/null +++ b/python/tests/test_mutate.py @@ -0,0 +1,58 @@ +"""The mutation harness generates well-formed, distinct mutants and +judges them through the real corpus runner.""" + +import ast +import shutil +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(REPO_ROOT / "tools")) + +import mutate # noqa: E402 + +MODULES = sorted(p.stem for p in mutate.PACKAGE.glob("*.py") if p.stem != "__init__") + + +def test_every_module_yields_parseable_distinct_mutants(): + mutants = mutate.inventory(set()) + assert {m.module for m in mutants} == set(MODULES) + ids = [m.id for m in mutants] + assert len(ids) == len(set(ids)) + for mutant in mutants: + ast.parse(mutant.source) + original = (mutate.PACKAGE / f"{mutant.module}.py").read_text() + assert ast.unparse(ast.parse(original)) != mutant.source, mutant.id + + +def test_constant_mutant_changes_exactly_one_constant(): + mutants = [m for m in mutate.inventory({"costs"}) if m.description == "20 -> 21"] + quote = [m for m in mutants if "QUOTE_COST = 21" in m.source] + assert quote, "QUOTE_COST = 20 should yield a 21 mutant" + diff = mutate.mutant_diff(quote[0]) + changed = [ + line + for line in diff.splitlines() + if line[:1] in "+-" and not line.startswith(("---", "+++")) + ] + assert changed == ["-QUOTE_COST = 20", "+QUOTE_COST = 21"] + + +def test_mirror_shares_data_and_copies_code(): + root = mutate._build_mirror() + try: + assert (root / "vectors").is_symlink() + assert (root / "puzzles").is_symlink() + assert not (root / "python" / "bitlisp").is_symlink() + assert (root / "tools" / "run_vectors.py").is_file() + finally: + shutil.rmtree(root) + + +def test_corpus_kills_a_broken_error_code_table(): + # Any mutant of the error-code table breaks the first vector file + # the corpus opens, so this integration test stays fast. + mutants = mutate.inventory({"errors"}) + broken = next(m for m in mutants if m.description != "raise deleted") + mutant_id, corpus, suite = mutate.evaluate(broken, timeout=120, tests=False) + assert (mutant_id, corpus, suite) == (broken.id, "killed", None) diff --git a/tools/mutate.py b/tools/mutate.py new file mode 100644 index 0000000..4a789d1 --- /dev/null +++ b/tools/mutate.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python3 +"""Mutation harness: does the vector corpus guard every line of the reference? + +Generates small semantic mutants of every module in python/bitlisp +(a flipped comparison, a swapped arithmetic operator, an off-by-one +constant, a deleted raise, a negated branch) and runs the vector +corpus against each one. A mutant the corpus fails is killed. A +mutant the corpus passes survived: either no vector pins the behavior +that line implements, or the mutant is equivalent to the original. +Both readings need a human, so every survivor is reported with its +diff. + +The corpus is the source of truth between sessions, so the corpus is +the primary oracle. With --tests, each survivor is additionally run +through the pytest suite (hypothesis invariants, oracle differentials, +unit tests), separating survivors nothing catches from survivors the +tests catch but the corpus does not. The first class is a gap in the +tests too. The second is a missing vector. + +Each worker runs in its own mirror of the repository under a temporary +directory (python/ and tools/ copied, vectors/ and puzzles/ linked), +so mutants never touch the checkout and workers never see each +other's edits. The unmutated tree must pass every oracle the mutants +face before any mutant runs. + + tools/mutate.py run everything, summary on stdout + tools/mutate.py --list print the mutant inventory, run nothing + tools/mutate.py --module costs restrict to one module + tools/mutate.py --only ID run one mutant and print its diff + tools/mutate.py --tests second pass over survivors + tools/mutate.py --report out.json machine-readable results + +Exit status: 0 when every mutant was killed, 1 when any survived, 2 +on a harness error (baseline failure, bad arguments). +""" + +import argparse +import ast +import difflib +import json +import os +import shutil +import subprocess +import sys +import tempfile +from concurrent.futures import ProcessPoolExecutor +from dataclasses import asdict, dataclass +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +PACKAGE = REPO_ROOT / "python" / "bitlisp" + +COMPARE_SWAPS = { + ast.Lt: ast.LtE, + ast.LtE: ast.Lt, + ast.Gt: ast.GtE, + ast.GtE: ast.Gt, + ast.Eq: ast.NotEq, + ast.NotEq: ast.Eq, + ast.Is: ast.IsNot, + ast.IsNot: ast.Is, + ast.In: ast.NotIn, + ast.NotIn: ast.In, +} + +BINOP_SWAPS = { + ast.Add: ast.Sub, + ast.Sub: ast.Add, + ast.Mult: ast.Add, + ast.FloorDiv: ast.Mult, + ast.Mod: ast.FloorDiv, + ast.LShift: ast.RShift, + ast.RShift: ast.LShift, + ast.BitAnd: ast.BitOr, + ast.BitOr: ast.BitAnd, + ast.BitXor: ast.BitAnd, +} + + +@dataclass(frozen=True) +class Mutant: + """One mutation site: which module, which line, what changed.""" + + id: str + module: str + line: int + description: str + source: str + + +def _op_name(op): + return type(op).__name__ + + +class _Sites(ast.NodeVisitor): + """Enumerates mutation sites in one module, in source order.""" + + def __init__(self): + self.sites = [] + + def _add(self, node, description, mutate): + self.sites.append((node.lineno, len(self.sites), description, mutate)) + + def visit_Compare(self, node): + for index, op in enumerate(node.ops): + swap = COMPARE_SWAPS.get(type(op)) + if swap is not None: + + def mutate(index=index, swap=swap): + node.ops[index] = swap() + + self._add(node, f"{_op_name(op)} -> {swap.__name__}", mutate) + self.generic_visit(node) + + def visit_BinOp(self, node): + swap = BINOP_SWAPS.get(type(node.op)) + if swap is not None: + + def mutate(swap=swap): + node.op = swap() + + self._add(node, f"{_op_name(node.op)} -> {swap.__name__}", mutate) + self.generic_visit(node) + + def visit_BoolOp(self, node): + swap = ast.Or if isinstance(node.op, ast.And) else ast.And + + def mutate(): + node.op = swap() + + self._add(node, f"{_op_name(node.op)} -> {swap.__name__}", mutate) + self.generic_visit(node) + + def visit_Constant(self, node): + value = node.value + if isinstance(value, bool): + + def mutate(): + node.value = not value + + self._add(node, f"{value} -> {not value}", mutate) + elif isinstance(value, int): + for delta in (1, -1): + + def mutate(delta=delta): + node.value = value + delta + + self._add(node, f"{value} -> {value + delta}", mutate) + + def visit_If(self, node): + test = node.test + + def mutate(): + node.test = ast.UnaryOp(op=ast.Not(), operand=test) + + self._add(node, "if test negated", mutate) + self.generic_visit(node) + + def visit_Raise(self, node): + def mutate(): + node.__class__ = ast.Pass + node.__dict__.clear() + + self._add(node, "raise deleted", mutate) + self.generic_visit(node) + + def visit_Break(self, node): + def mutate(): + node.__class__ = ast.Continue + + self._add(node, "break -> continue", mutate) + + def visit_Continue(self, node): + def mutate(): + node.__class__ = ast.Break + + self._add(node, "continue -> break", mutate) + + +class _NotRemover(ast.NodeTransformer): + """Rewrites one chosen `not x` to `x`.""" + + def __init__(self, target): + self.target = target + + def visit_UnaryOp(self, node): + if node is self.target: + return node.operand + return self.generic_visit(node) + + +def _not_sites(tree): + """Mutation sites for `not` removal, applied by transformer.""" + sites = [] + for node in ast.walk(tree): + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): + sites.append(node) + return sites + + +def generate(module_path): + """Yields every mutant of one module, deterministically ordered.""" + source = module_path.read_text(encoding="utf-8") + name = module_path.stem + baseline = ast.parse(source) + visitor = _Sites() + visitor.visit(baseline) + total = len(visitor.sites) + for ordinal in range(total): + tree = ast.parse(source) + visitor = _Sites() + visitor.visit(tree) + line, _, description, mutate = visitor.sites[ordinal] + mutate() + ast.fix_missing_locations(tree) + yield Mutant( + id=f"{name}:{ordinal}", + module=name, + line=line, + description=description, + source=ast.unparse(tree), + ) + for ordinal, target in enumerate(_not_sites(ast.parse(source))): + tree = ast.parse(source) + target = _not_sites(tree)[ordinal] + tree = _NotRemover(target).visit(tree) + ast.fix_missing_locations(tree) + yield Mutant( + id=f"{name}:not{ordinal}", + module=name, + line=target.lineno, + description="not removed", + source=ast.unparse(tree), + ) + + +def inventory(modules): + mutants = [] + for path in sorted(PACKAGE.glob("*.py")): + if modules and path.stem not in modules: + continue + mutants.extend(generate(path)) + return mutants + + +def mutant_diff(mutant): + """Unified diff between the module and the mutant, both unparsed + so that formatting differences never appear.""" + original = ast.unparse(ast.parse((PACKAGE / f"{mutant.module}.py").read_text())) + return "".join( + difflib.unified_diff( + original.splitlines(keepends=True), + mutant.source.splitlines(keepends=True), + fromfile=f"{mutant.module}.py", + tofile=f"{mutant.module}.py ({mutant.id})", + n=1, + ) + ) + + +# Worker side. Each process builds one mirror and reuses it for every +# mutant it is handed. + +_MIRROR = None +_CORPUS_DRIVER = """ +import sys +sys.path.insert(0, sys.argv[1]) +import run_vectors +for path in run_vectors.discover(): + try: + run_vectors.run_file(path) + except run_vectors.VectorError as exc: + print(exc) + sys.exit(1) +""" + + +def _build_mirror(): + root = Path(tempfile.mkdtemp(prefix="bitlisp-mutate-")) + ignore = shutil.ignore_patterns("__pycache__", "*.egg-info", ".hypothesis") + shutil.copytree(REPO_ROOT / "python", root / "python", ignore=ignore) + shutil.copytree(REPO_ROOT / "tools", root / "tools", ignore=ignore) + shutil.copy(REPO_ROOT / "pyproject.toml", root / "pyproject.toml") + for shared in ("vectors", "puzzles"): + os.symlink(REPO_ROOT / shared, root / shared) + return root + + +def _mirror(): + global _MIRROR + if _MIRROR is None: + _MIRROR = _build_mirror() + return _MIRROR + + +def _install(root, mutant): + (root / "python" / "bitlisp" / f"{mutant.module}.py").write_text( + mutant.source, encoding="utf-8" + ) + for cache in (root / "python" / "bitlisp").glob("__pycache__/*"): + cache.unlink() + + +def _restore(root, module): + shutil.copy(PACKAGE / f"{module}.py", root / "python" / "bitlisp" / f"{module}.py") + + +def _run(root, argv, timeout): + """Runs argv in the mirror. Returns 'killed', 'survived', or 'timeout'.""" + env = dict(os.environ, PYTHONPATH=str(root / "python"), PYTHONDONTWRITEBYTECODE="1") + try: + proc = subprocess.run( + argv, + cwd=root, + env=env, + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + return "timeout" + return "survived" if proc.returncode == 0 else "killed" + + +def run_corpus(root, timeout): + argv = [sys.executable, "-c", _CORPUS_DRIVER, str(root / "tools")] + return _run(root, argv, timeout) + + +def run_tests(root, timeout): + argv = [ + sys.executable, + "-m", + "pytest", + str(root / "python" / "tests"), + "-q", + "-x", + "-p", + "no:cacheprovider", + ] + return _run(root, argv, timeout) + + +def evaluate(mutant, timeout, tests): + root = _mirror() + _install(root, mutant) + try: + corpus = run_corpus(root, timeout) + suite = None + if tests and corpus == "survived": + suite = run_tests(root, timeout * 10) + finally: + _restore(root, mutant.module) + return mutant.id, corpus, suite + + +def _evaluate_star(args): + return evaluate(*args) + + +def baseline_passes(timeout, tests): + """The unmutated tree must pass every oracle the mutants face, + else a broken mirror would report every mutant killed.""" + root = _build_mirror() + try: + if run_corpus(root, timeout) != "survived": + return False + return not tests or run_tests(root, timeout * 10) == "survived" + finally: + shutil.rmtree(root) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--module", + action="append", + default=[], + help="restrict to a module (repeatable)", + ) + parser.add_argument( + "--only", + action="append", + default=[], + help="run only this mutant id (repeatable), printing its diff when alone", + ) + parser.add_argument( + "--list", action="store_true", help="print the inventory and exit" + ) + parser.add_argument( + "--tests", + action="store_true", + help="run the pytest suite over corpus survivors", + ) + parser.add_argument("--jobs", type=int, default=max(1, (os.cpu_count() or 2) - 2)) + parser.add_argument( + "--timeout", type=float, default=120.0, help="seconds per corpus run" + ) + parser.add_argument("--report", type=Path, help="write results as JSON") + args = parser.parse_args() + + modules = set(args.module) + if args.only: + modules = {mutant_id.split(":")[0] for mutant_id in args.only} + mutants = inventory(modules) + if args.only: + wanted = set(args.only) + mutants = [m for m in mutants if m.id in wanted] + if missing := wanted - {m.id for m in mutants}: + print(f"no mutant {sorted(missing)}", file=sys.stderr) + return 2 + if len(mutants) == 1: + print(mutant_diff(mutants[0])) + if args.list: + for m in mutants: + print(f"{m.id:<20} {m.module}.py:{m.line:<5} {m.description}") + print(f"{len(mutants)} mutant(s)") + return 0 + + if not baseline_passes(args.timeout, args.tests): + print( + "the unmutated tree fails its oracle, refusing to mutate", file=sys.stderr + ) + return 2 + + by_id = {m.id: m for m in mutants} + results = [] + work = [(m, args.timeout, args.tests) for m in mutants] + with ProcessPoolExecutor(max_workers=args.jobs) as pool: + for done, (mutant_id, corpus, suite) in enumerate( + pool.map(_evaluate_star, work, chunksize=4), start=1 + ): + results.append((mutant_id, corpus, suite)) + if done % 50 == 0 or done == len(work): + print(f"{done}/{len(work)}", file=sys.stderr) + + survivors = [r for r in results if r[1] == "survived"] + timeouts = [r for r in results if r[1] == "timeout"] + killed = len(results) - len(survivors) - len(timeouts) + + per_module = {} + for mutant_id, corpus, _ in results: + entry = per_module.setdefault(by_id[mutant_id].module, [0, 0, 0]) + entry[{"killed": 0, "survived": 1, "timeout": 2}[corpus]] += 1 + print(f"{'module':<14} {'mutants':>7} {'killed':>7} {'survived':>8} {'timeout':>7}") + for module, (k, s, t) in sorted(per_module.items()): + print(f"{module:<14} {k + s + t:>7} {k:>7} {s:>8} {t:>7}") + total = len(results) + print( + f"{'total':<14} {total:>7} {killed:>7} {len(survivors):>8} {len(timeouts):>7}" + ) + + if survivors: + print("\nsurvivors:") + for mutant_id, _, suite in survivors: + m = by_id[mutant_id] + tag = "" if suite is None else f" [tests: {suite}]" + print(f" {m.id:<20} {m.module}.py:{m.line:<5} {m.description}{tag}") + if timeouts: + print("\ntimeouts:") + for mutant_id, _, _ in timeouts: + m = by_id[mutant_id] + print(f" {m.id:<20} {m.module}.py:{m.line:<5} {m.description}") + + if args.report: + report = [ + dict( + asdict(by_id[mutant_id]), + corpus=corpus, + tests=suite, + diff=mutant_diff(by_id[mutant_id]), + ) + for mutant_id, corpus, suite in results + ] + for entry in report: + del entry["source"] + args.report.write_text(json.dumps(report, indent=1), encoding="utf-8") + + return 1 if survivors else 0 + + +if __name__ == "__main__": + sys.exit(main()) From cd8969dd33b55d52616defe2bccd9a96bd9fe6ef Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 23 Aug 2026 19:57:56 -0700 Subject: [PATCH 2/7] vectors: the gaps the first mutation pass found Nineteen cases, each a behavior the spec states that no vector exercised, found by tools/mutate.py as mutants the corpus let survive. Every case passes the reference. The two path cases were cross-checked against the consensus oracle (chia-rs, flags 0), the three seal cases against the vendored Bitcoin Core framework. VM.md section 3.1: an interior zero byte in a path atom is not a leading zero byte and is not costed (path_interior_zero_byte, path_leading_and_interior_zero_bytes). The surviving mutant counted every zero byte. VM.md section 2 (D5): the one-byte atom 0x7f in the long form is non-minimal (nonminimal_one_byte_atom_7f), a lone 0xfc prefix byte is bad_encoding (lone_prefix_fc), and a length of 8,191 in the three-byte form is non-minimal (nonminimal_length_e0_at_8191). The corpus pinned the 0x40 boundary but not the 0x2000 one. CONDITIONS.md time asserts: ASSERT_SEQUENCE_HEIGHT accepts 0 and rejects -1 (seqheight_zero, seqheight_negative). The other three time asserts had both cases, this one had neither. CONDITIONS.md section 1, VALIDATION.md rule 6: a reserved declared cost of exactly -1 is bad_condition_arg, not reserved_cost_too_low (reserved_cost_minus_one). The corpus had -500. CONDITIONS.md self asserts: ASSERT_MY_SCRIPTPUBKEY and ASSERT_MY_AMOUNT take exactly one operand (four arity cases). Only the outpoint and taproot asserts had arity cases. CONDITIONS.md message family: a pair in a non-amount specifier field is bad_condition_arg (assure_script_specifier_pair), an empty ASSERT_ANNOUNCEMENT is bad_condition_arity (assert_announcement_arity_zero), and a specifier composing amount with tapleaf parses its fields in operand order (assure_amount_tapleaf_specifier_parses). No composed case had carried an amount before an identity field. VALIDATION.md rule 3 (C9): an amount specifier over a zero-amount prevout balances (amount_specifier_over_zero_amount_input_balances). The amount domain is 0 to MAX_MONEY and the corpus started at 1. CONDITIONS.md seals: the compact-size boundary at 253 in the txid and outputs hash, a 252-byte and a 253-byte output script under SEAL_OUTPUTS and 253 outputs under SEAL. The one-byte form was the only one any seal vector exercised. --- vectors/README.md | 10 + vectors/conditions/encoding.json | 7 + vectors/conditions/messages.json | 34 + vectors/conditions/self-asserts.json | 28 + vectors/conditions/time-asserts.json | 19 + vectors/validation/messages.json | 38 + vectors/validation/seals.json | 1092 ++++++++++++++++++++++++++ vectors/vm/paths.json | 18 + vectors/vm/serialize.json | 24 + 9 files changed, 1270 insertions(+) diff --git a/vectors/README.md b/vectors/README.md index 39df3c3..b35108e 100644 --- a/vectors/README.md +++ b/vectors/README.md @@ -133,3 +133,13 @@ duplicate-CREATE_OUTPUT theft case as vector #1 in Run the corpus with `python3 tools/run_vectors.py`. A vector file whose suite has no runner yet fails loudly rather than being skipped. + +## Mutation coverage + +`tools/mutate.py` asks the converse question of every vector file: +does the corpus fail when the reference is wrong? It generates small +semantic mutants of `python/bitlisp/` and runs the corpus against +each. A surviving mutant is a behavior no vector pins, or a mutant +equivalent to the original. Each pass is triaged in +`docs/mutation-triage.md`, and every gap it finds becomes a vector +here with its spec citation, the same day, like any other behavior. diff --git a/vectors/conditions/encoding.json b/vectors/conditions/encoding.json index 0ad3025..1eb5a9e 100644 --- a/vectors/conditions/encoding.json +++ b/vectors/conditions/encoding.json @@ -309,6 +309,13 @@ "expect": { "error": "bad_condition_arg" } + }, + { + "name": "reserved_cost_minus_one", + "conditions": "ffff8180ff81ff8080", + "expect": { + "error": "bad_condition_arg" + } } ] } diff --git a/vectors/conditions/messages.json b/vectors/conditions/messages.json index 3768bf2..b504354 100644 --- a/vectors/conditions/messages.json +++ b/vectors/conditions/messages.json @@ -588,6 +588,40 @@ "expect": { "error": "bad_condition_arg" } + }, + { + "name": "assure_script_specifier_pair", + "conditions": "ffff42ff8200e2ff826869ffff01028080", + "expect": { + "error": "bad_condition_arg" + } + }, + { + "name": "assert_announcement_arity_zero", + "conditions": "ffff418080", + "expect": { + "error": "bad_condition_arity" + } + }, + { + "name": "assure_amount_tapleaf_specifier_parses", + "conditions": "ffff42ff09ff826869ff8203e8ffa02b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b8080", + "expect": { + "parsed": [ + { + "opcode": 66, + "assurer_commitment": 0, + "requirer": { + "commitment": 9, + "fields": [ + 1000, + "2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b" + ] + }, + "message": "6869" + } + ] + } } ] } diff --git a/vectors/conditions/self-asserts.json b/vectors/conditions/self-asserts.json index ba440b4..2a79000 100644 --- a/vectors/conditions/self-asserts.json +++ b/vectors/conditions/self-asserts.json @@ -411,6 +411,34 @@ "expect": { "error": "bad_condition_opcode" } + }, + { + "name": "scriptpubkey_arity_zero", + "conditions": "ffff328080", + "expect": { + "error": "bad_condition_arity" + } + }, + { + "name": "scriptpubkey_arity_two", + "conditions": "ffff32ffa25120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaffa25120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa8080", + "expect": { + "error": "bad_condition_arity" + } + }, + { + "name": "amount_arity_zero", + "conditions": "ffff338080", + "expect": { + "error": "bad_condition_arity" + } + }, + { + "name": "amount_arity_two", + "conditions": "ffff33ff8203e8ff8203e88080", + "expect": { + "error": "bad_condition_arity" + } } ] } diff --git a/vectors/conditions/time-asserts.json b/vectors/conditions/time-asserts.json index 24169b9..a3baa9c 100644 --- a/vectors/conditions/time-asserts.json +++ b/vectors/conditions/time-asserts.json @@ -199,6 +199,25 @@ "expect": { "error": "bad_condition_arg" } + }, + { + "name": "seqheight_zero", + "conditions": "ffff22ff808080", + "expect": { + "parsed": [ + { + "opcode": 34, + "blocks": 0 + } + ] + } + }, + { + "name": "seqheight_negative", + "conditions": "ffff22ff81ff8080", + "expect": { + "error": "bad_condition_arg" + } } ] } diff --git a/vectors/validation/messages.json b/vectors/validation/messages.json index d2e8d83..b5f7f04 100644 --- a/vectors/validation/messages.json +++ b/vectors/validation/messages.json @@ -1448,6 +1448,44 @@ "expect": { "error": "unsatisfied_announcement_assert" } + }, + { + "name": "amount_specifier_over_zero_amount_input_balances", + "tx": { + "version": 2, + "locktime": 0, + "inputs": [ + { + "txid": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": 0, + "script_pubkey": "5120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": 0, + "conditions": "ffff42ff27ff826869ffa4bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb000000008080", + "tapleaf": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", + "merkle_root": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "internal_key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + }, + { + "txid": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "index": 0, + "script_pubkey": "5120bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "amount": 1000, + "conditions": "ffff43ff27ff826869ff808080", + "tapleaf": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", + "merkle_root": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "internal_key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + } + ], + "outputs": [ + { + "script_pubkey": "00149999999999999999999999999999999999999999", + "amount": 500 + } + ] + }, + "expect": { + "valid": true + } } ] } diff --git a/vectors/validation/seals.json b/vectors/validation/seals.json index ac4cbef..e6f7f0b 100644 --- a/vectors/validation/seals.json +++ b/vectors/validation/seals.json @@ -737,6 +737,1098 @@ "expect": { "valid": true } + }, + { + "name": "seal_outputs_script_252_bytes_one_byte_length", + "tx": { + "version": 2, + "locktime": 0, + "inputs": [ + { + "txid": "1111111111111111111111111111111111111111111111111111111111111111", + "index": 0, + "script_pubkey": "5120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": 1000, + "conditions": "ffff61ffa00b9c34405b16cb0ffaa71316779b7e1513028bd7d732edb5d8ce212ca004f7f48080", + "tapleaf": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", + "merkle_root": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "internal_key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + } + ], + "outputs": [ + { + "script_pubkey": "6acccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "amount": 400 + } + ] + }, + "expect": { + "valid": true + } + }, + { + "name": "seal_outputs_script_253_bytes_three_byte_length", + "tx": { + "version": 2, + "locktime": 0, + "inputs": [ + { + "txid": "1111111111111111111111111111111111111111111111111111111111111111", + "index": 0, + "script_pubkey": "5120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": 1000, + "conditions": "ffff61ffa0430b99e7b40d8989ab44a9c14f35ff4eff3a60a9f91e4d519b708fd59cdb96e58080", + "tapleaf": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", + "merkle_root": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "internal_key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + } + ], + "outputs": [ + { + "script_pubkey": "6acccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "amount": 400 + } + ] + }, + "expect": { + "valid": true + } + }, + { + "name": "seal_txid_253_outputs_three_byte_count", + "tx": { + "version": 2, + "locktime": 0, + "inputs": [ + { + "txid": "1111111111111111111111111111111111111111111111111111111111111111", + "index": 0, + "script_pubkey": "5120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "amount": 1000, + "conditions": "ffff60ffa047f77dff9478e4755b52291963d8800bf6167c4e9c9e6c6e76d02488c9456df78080", + "tapleaf": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", + "merkle_root": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "internal_key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" + } + ], + "outputs": [ + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + }, + { + "script_pubkey": "6a", + "amount": 1 + } + ] + }, + "expect": { + "valid": true + } } ] } diff --git a/vectors/vm/paths.json b/vectors/vm/paths.json index ba97dc3..19e7a11 100644 --- a/vectors/vm/paths.json +++ b/vectors/vm/paths.json @@ -154,6 +154,24 @@ "result": "80", "cost": 44 } + }, + { + "name": "path_interior_zero_byte", + "program": "820100", + "env": "ffffffffffffffff0a8080808080808080", + "expect": { + "result": "0a", + "cost": 76 + } + }, + { + "name": "path_leading_and_interior_zero_bytes", + "program": "83000100", + "env": "ffffffffffffffff0a8080808080808080", + "expect": { + "result": "0a", + "cost": 80 + } } ] } diff --git a/vectors/vm/serialize.json b/vectors/vm/serialize.json index 9e52bcb..ca94ea2 100644 --- a/vectors/vm/serialize.json +++ b/vectors/vm/serialize.json @@ -226,6 +226,30 @@ "expect": { "error": "bad_encoding" } + }, + { + "name": "nonminimal_one_byte_atom_7f", + "program": "ff10ffff01817fffff010380", + "env": "80", + "expect": { + "error": "bad_encoding" + } + }, + { + "name": "lone_prefix_fc", + "program": "fc", + "env": "80", + "expect": { + "error": "bad_encoding" + } + }, + { + "name": "nonminimal_length_e0_at_8191", + "program": "e01fffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "env": "80", + "expect": { + "error": "bad_encoding" + } } ] } From c3fa730b2a6fb2d62ec4ac210761ab11efcaf0ab Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 23 Aug 2026 21:11:49 -0700 Subject: [PATCH 3/7] docs: the mutation triage record docs/mutation-triage.md records what tools/mutate.py found on its first pass and how each survivor was judged: the method, the six survivor classes (equivalent, unreachable guard, same code, model precondition, beyond reach, gap) with the reason each of the first five is accepted without a vector, the per-module numbers on main at PR 60 (1,733 mutants, 1,551 killed, 179 corpus survivors of which the pytest suite kills 55, 3 timeouts), the nineteen gaps with the mutant that exposed each and the vector that closes it, and the representative sites of every accepted survivor. Line numbers are left out on purpose: the classes are what a later pass compares against, and the re-running section says how. docs/README.md gains the entry. --- docs/README.md | 3 + docs/mutation-triage.md | 171 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 docs/mutation-triage.md diff --git a/docs/README.md b/docs/README.md index a493e73..d228009 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,9 @@ the public record to a Lisp VM for Bitcoin or to the conditions architecture, with BitLisp's answer, its status, and where it lives. The skeleton of the Phase 5 essay. +- [mutation-triage.md](mutation-triage.md): what the mutation + harness found when run against the vector corpus, the survivor + classes accepted and why, and the vectors each pass added. - [condition-comparison.md](condition-comparison.md): informative side-by-side of Chia's deployed condition vocabulary and BitLisp's v0 vocabulary, plus the validation-layer architecture comparison. diff --git a/docs/mutation-triage.md b/docs/mutation-triage.md new file mode 100644 index 0000000..940017e --- /dev/null +++ b/docs/mutation-triage.md @@ -0,0 +1,171 @@ +# Mutation triage + +The vector corpus is the source of truth between sessions (ground +rule 2), which makes its strength a question worth measuring. The +diff harness measures the reference against the oracles. The +mutation harness, `tools/mutate.py`, measures the corpus against the +reference: it breaks the reference in one small way at a time and +asks whether the corpus notices. This record keeps the results of +each pass, the survivor classes accepted with their rationale, and +the vectors each pass added. It is the corpus-side counterpart of +the divergence review in the two record docs. + +## Method + +The harness generates every mutant of every module in +`python/bitlisp/` from a fixed set of edits: each comparison operator +flipped to its neighbor or negation, each arithmetic and bitwise +operator swapped, each `and` and `or` exchanged, each integer +constant moved by one and each boolean flipped, each `if` test +negated, each `not` removed, each `raise` deleted, and `break` and +`continue` exchanged. Each mutant runs the whole corpus in a private +mirror of the tree. A mutant the corpus fails is killed. One the +corpus passes survived and is triaged by hand into a class below. +With `--tests`, every corpus survivor also runs the pytest suite +(hypothesis invariants, oracle differentials, unit tests), which +separates survivors nothing catches from survivors the tests catch +but the corpus does not. + + .venv/bin/python tools/mutate.py --tests --report mutants.json + +The run refuses to start unless the unmutated tree passes every +oracle the mutants face. Timeouts count as kills: a mutant that +hangs is a mutant the budget eventually rejects, at a cost in wall +clock the harness will not pay. + +## Survivor classes + +A survivor lands in exactly one class. The first five are accepted +without a vector, each for a stated reason. The sixth is the finding +the harness exists for. + +| class | meaning | accepted because | +| --- | --- | --- | +| equivalent | the mutant computes the same function on every input | nothing to pin: the constant is a private tag, the index is `[0]` against `[-1]` on a one-element list, the shifted low bits are already set, the annotation is never evaluated, the frozen flag guards a hash nobody takes | +| unreachable guard | the deleted or flipped check cannot fire on any input the caller can supply | the guard defends an invariant of the implementation (a mistyped error code, a width the caller already checked, the apply backstop that the charge-before-completion invariant makes dead), not a rule of the spec | +| same code | the mutant changes which check reports a defect, never whether it is a defect | the corpus pins error codes, and two checks that share a code are indistinguishable by design: which of them fires is a diagnostic, and fail-fast is the contract (decision 22) | +| model precondition | the mutant weakens the transaction model's constructor in `tx.py` | a malformed model is a harness bug and raises `ValueError`, never a spend failure. The model is the reference's stand-in for base consensus, and every vector is a well-formed transaction by construction | +| beyond reach | the input that would distinguish the mutant cannot be built, or cannot be carried in a vector | the boundary sits above the 4 MB witness ceiling, needs a signature with a scalar in a range no signer can reach without infeasible work, or needs an atom too large for the corpus and is pinned by a unit test instead | +| gap | a behavior the spec states and no vector pins | fixed the same day: a vector with its spec citation | + +Not a class, but recorded: test-facing helpers exported from the +package whose only callers are the invariant suites (`condition_cost`, +which the cost invariants use to check that the meter's total equals +the per-condition sum) survive the corpus by construction and are +pinned by `--tests`. + +## Pass of 2026-08-23 + +Run against `main` at PR 60 (`7eee46f`) with `--tests`, the corpus +at 1,151 cases after this pass's nineteen vectors. + +| module | mutants | killed | survived | timeout | +| --- | --- | --- | --- | --- | +| conditions | 481 | 442 | 39 | 0 | +| costs | 114 | 114 | 0 | 0 | +| errors | 3 | 2 | 1 | 0 | +| machine | 80 | 75 | 5 | 0 | +| operators | 379 | 361 | 17 | 1 | +| secp256k1 | 153 | 137 | 15 | 1 | +| serialize | 160 | 125 | 34 | 1 | +| sexp | 37 | 37 | 0 | 0 | +| tx | 165 | 104 | 61 | 0 | +| validation | 161 | 154 | 7 | 0 | +| total | 1,733 | 1,551 | 179 | 3 | + +The numbers are the state after the nineteen vectors below landed. +The first pass, on the tree before PR 60 merged, had 195 survivors +out of 1,720 mutants. Of the 179 remaining, the +pytest suite kills 55: 29 of the 61 model preconditions in `tx.py`, +ten of the fifteen in `secp256k1.py` (the group order moved by one, +the width guards, the point-at-infinity branch), nine in +`serialize.py` (the length-form boundary at 2^20, the `bytes`-only +type check, two truncation checks), the error-code guard, +`condition_cost`, and a few `[0]` indices and flags elsewhere. The +other 124 survive both, the five in `machine.py` and all seven in +`validation.py` among them. All 179 fall into the accepted classes +below. The +three timeouts are mutants that loop until the budget rejects them: +`if` returning its else branch in both cases, a scalar multiplication +that never shifts its scalar, a deserializer that never advances. + +### Gaps found, vectors added + +Nineteen cases, each a behavior the spec states that no vector +exercised. All pass the reference, the two vm path cases were +cross-checked against the consensus oracle, and the three seal cases +against the vendored Bitcoin Core framework. + +| site | mutant that survived | vector | spec | +| --- | --- | --- | --- | +| path cost, leading zero bytes | `break` to `continue` in the leading-zero count: an interior zero byte was counted as leading | `vm/paths.json` `path_interior_zero_byte`, `path_leading_and_interior_zero_bytes` | VM.md section 3.1 | +| one-byte atom in the long form | `<= 0x7F` to `< 0x7F` and `0x7F` to `0x7E`: `0x81 0x7F` accepted as canonical | `vm/serialize.json` `nonminimal_one_byte_atom_7f` | VM.md section 2 (D5) | +| invalid prefix byte | `>= 0xFC` to `> 0xFC`: a lone `0xFC` fell through every length form | `vm/serialize.json` `lone_prefix_fc` | VM.md section 2 (D5) | +| three-byte length form floor | floor `0x2000` to `0x1FFF`: a length of 8,191 in the three-byte form accepted as minimal | `vm/serialize.json` `nonminimal_length_e0_at_8191` | VM.md section 2 (D5) | +| ASSERT_SEQUENCE_HEIGHT domain | low bound `0` to `1` and to `-1`: zero rejected, minus one accepted | `conditions/time-asserts.json` `seqheight_zero`, `seqheight_negative` | CONDITIONS.md time asserts | +| reserved declared cost | `cost < 0` to `cost < -1`: a declared cost of exactly -1 reported as `reserved_cost_too_low` instead of `bad_condition_arg` | `conditions/encoding.json` `reserved_cost_minus_one` | CONDITIONS.md section 1, VALIDATION.md rule 6 | +| ASSERT_MY_SCRIPTPUBKEY and ASSERT_MY_AMOUNT arity | the arity raise deleted: two operands accepted, the second ignored | `conditions/self-asserts.json` `scriptpubkey_arity_zero`, `scriptpubkey_arity_two`, `amount_arity_zero`, `amount_arity_two` | CONDITIONS.md self asserts | +| specifier field shape | the atom check on a non-amount specifier field deleted: a pair carried into the ledger | `conditions/messages.json` `assure_script_specifier_pair` | CONDITIONS.md message family | +| ASSERT_ANNOUNCEMENT arity | the arity raise deleted: an empty list crashed instead of reporting `bad_condition_arity` | `conditions/messages.json` `assert_announcement_arity_zero` | CONDITIONS.md message family | +| composed specifier operand order | `continue` to `break` after an amount field: the identity fields after it never parsed | `conditions/messages.json` `assure_amount_tapleaf_specifier_parses` | CONDITIONS.md message family | +| specifier amount domain | `0 <= value` to `0 < value`: a zero-amount prevout unaddressable by amount | `validation/messages.json` `amount_specifier_over_zero_amount_input_balances` | VALIDATION.md rule 3 (C9) | +| compact-size boundary in the txid and outputs hash | `n < 0xFD` to `n < 0xFC`: a 252-byte script or count encoded in the three-byte form | `validation/seals.json` `seal_outputs_script_252_bytes_one_byte_length`, `seal_outputs_script_253_bytes_three_byte_length`, `seal_txid_253_outputs_three_byte_count` | CONDITIONS.md seals | + +### Survivors accepted + +The accepted survivors by class, with the sites that represent +them. Line numbers are omitted on purpose: the mutant inventory is +regenerated from the tree, and the classes are what a later pass +compares against. + +**Equivalent.** The stepper tags in `machine.py` and the parser +tasks in `serialize.py` are private constants whose only property is +distinctness. Every `args[0]` after an arity check of one reads the +same element as `args[-1]`, as does `values[0]` on the final +one-element stack and `opcode_atom[0]` on a one-byte atom. The +serializer's length-form bound already has every low bit set, so +widening the mask term changes nothing, and a shift by zero is a +shift by zero in either direction. The dataclass `frozen` flags +guard a hash that no rule takes (matching is by index and by +equality). The `bytes | None` annotations in `tx.py` are deferred +under Python 3.14 and never evaluated. In `secp256k1.py`, +`(P + 2) // 4` equals `(P + 1) // 4` because `P + 1` is divisible by +4, and `lift_x(P)` returns `None` under either comparison because 7 +is not a quadratic residue modulo `P`. + +**Unreachable guard.** The `ValueError` on a mistyped error code in +`errors.py`, the apply backstop in `machine.py` that the +charge-before-completion invariant makes dead, the width checks in +`secp256k1.py` whose callers guarantee widths, the `AssertionError` +branches on an unknown specifier or binding kind in `validation.py`, +and the `bytes`-only type check at the deserializer's door. + +**Same code.** Every `bad_encoding` sub-case in the deserializer +(a deleted truncation check falls into the next one), the `is_atom` +checks shadowed by width checks in the condition parsers (a pair has +length two), the first arity check of the two variadic message +parsers (the mode-derived count check reports the same code), the +`(empty)` fallbacks in error messages, and the `name` property's +table index, which only names a condition in a message. + +**Model precondition.** Every constructor check in `tx.py`: field +ranges, byte types, the non-empty input and output tuples, distinct +outpoints, and value conservation. + +**Beyond reach.** The length-form boundaries at 2^20 and 2^27 bytes +(the first is pinned by `test_serialize.py`, the second lies above +the 4 MB witness ceiling), the prefix byte `0xFB` (a 12 GB atom), +and the compact-size two-byte and four-byte forms in the txid (a +65,536-byte script or 65,536 outputs, and the eight-byte form beyond +that). In `secp256k1.py`, a 64-byte signature with `s` at or above +the group order `N` reduces to a scalar below 2^256 minus `N`, which +no signer reaches without about 2^128 work, `r` at or above `P` +likewise, and a tweak scalar of exactly `N` is a hash preimage. The +group order itself moved by one changes only those checks. + +## Re-running + +A pass belongs with any change to `python/bitlisp/` that adds a +branch, and at each recurring checkpoint. Compare the survivor list +against the classes above: a survivor that fits none is a gap, and a +survivor in the gap table above is a regression in the corpus. From 5bc7cdd53893069d7ac56659ec413b2edbefe8bf Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 23 Aug 2026 21:26:52 -0700 Subject: [PATCH 4/7] tools: crashes counted apart from kills A mutant that makes the reference raise outside its error taxonomy stops the corpus runner on an escaping exception before any vector reaches a verdict. That is detection, but by Python rather than by the corpus, and counting it as a kill overstates what the corpus guards. The corpus driver now exits with its own code on a vector's verdict, so any other nonzero exit is classified crashed, and the pytest pass treats only exit 1 (failing tests) as a kill. The summary gains the column and the JSON report carries the verdict. The unit test pins the mapping. --- python/tests/test_mutate.py | 16 ++++++++++ tools/mutate.py | 59 ++++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/python/tests/test_mutate.py b/python/tests/test_mutate.py index 4335a4f..73d5a28 100644 --- a/python/tests/test_mutate.py +++ b/python/tests/test_mutate.py @@ -56,3 +56,19 @@ def test_corpus_kills_a_broken_error_code_table(): broken = next(m for m in mutants if m.description != "raise deleted") mutant_id, corpus, suite = mutate.evaluate(broken, timeout=120, tests=False) assert (mutant_id, corpus, suite) == (broken.id, "killed", None) + + +def test_verdict_separates_the_oracle_from_an_escaping_exception(): + # Exit 0 survives, the oracle's own code kills, any other exit is + # an exception escaping the reference and counts apart. + root = mutate._build_mirror() + try: + verdicts = { + code: mutate._run( + root, [sys.executable, "-c", f"raise SystemExit({code})"], 30, {3} + ) + for code in (0, 3, 1) + } + finally: + shutil.rmtree(root) + assert verdicts == {0: "survived", 3: "killed", 1: "crashed"} diff --git a/tools/mutate.py b/tools/mutate.py index 4a789d1..4b6d8f2 100644 --- a/tools/mutate.py +++ b/tools/mutate.py @@ -8,7 +8,11 @@ mutant the corpus passes survived: either no vector pins the behavior that line implements, or the mutant is equivalent to the original. Both readings need a human, so every survivor is reported with its -diff. +diff. A mutant that makes the reference raise something outside its +error taxonomy, so the runner stops on an escaping exception rather +than a vector's verdict, crashed: detected, but by Python rather than +by the corpus, and counted apart so the corpus's own coverage is not +overstated. The corpus is the source of truth between sessions, so the corpus is the primary oracle. With --tests, each survivor is additionally run @@ -30,8 +34,8 @@ tools/mutate.py --tests second pass over survivors tools/mutate.py --report out.json machine-readable results -Exit status: 0 when every mutant was killed, 1 when any survived, 2 -on a harness error (baseline failure, bad arguments). +Exit status: 0 when every mutant was killed or crashed, 1 when any +survived, 2 on a harness error (baseline failure, bad arguments). """ import argparse @@ -262,7 +266,11 @@ def mutant_diff(mutant): # mutant it is handed. _MIRROR = None -_CORPUS_DRIVER = """ +# The corpus driver exits with this code on a vector's verdict. Any +# other nonzero exit is an exception escaping the reference. +VECTOR_FAILURE = 3 +_CORPUS_DRIVER = f""" +VECTOR_FAILURE = {VECTOR_FAILURE} import sys sys.path.insert(0, sys.argv[1]) import run_vectors @@ -271,7 +279,7 @@ def mutant_diff(mutant): run_vectors.run_file(path) except run_vectors.VectorError as exc: print(exc) - sys.exit(1) + sys.exit(VECTOR_FAILURE) """ @@ -305,8 +313,10 @@ def _restore(root, module): shutil.copy(PACKAGE / f"{module}.py", root / "python" / "bitlisp" / f"{module}.py") -def _run(root, argv, timeout): - """Runs argv in the mirror. Returns 'killed', 'survived', or 'timeout'.""" +def _run(root, argv, timeout, verdict_codes): + """Runs argv in the mirror. Returns 'survived' on exit 0, 'killed' on + an exit code in verdict_codes (the oracle judged the mutant), + 'crashed' on any other exit, or 'timeout'.""" env = dict(os.environ, PYTHONPATH=str(root / "python"), PYTHONDONTWRITEBYTECODE="1") try: proc = subprocess.run( @@ -319,12 +329,14 @@ def _run(root, argv, timeout): ) except subprocess.TimeoutExpired: return "timeout" - return "survived" if proc.returncode == 0 else "killed" + if proc.returncode == 0: + return "survived" + return "killed" if proc.returncode in verdict_codes else "crashed" def run_corpus(root, timeout): argv = [sys.executable, "-c", _CORPUS_DRIVER, str(root / "tools")] - return _run(root, argv, timeout) + return _run(root, argv, timeout, {VECTOR_FAILURE}) def run_tests(root, timeout): @@ -338,7 +350,9 @@ def run_tests(root, timeout): "-p", "no:cacheprovider", ] - return _run(root, argv, timeout) + # pytest exits 1 on failing tests. Its other codes (interrupted, + # internal error, usage error, no tests collected) judge nothing. + return _run(root, argv, timeout, {1}) def evaluate(mutant, timeout, tests): @@ -436,19 +450,24 @@ def main(): survivors = [r for r in results if r[1] == "survived"] timeouts = [r for r in results if r[1] == "timeout"] - killed = len(results) - len(survivors) - len(timeouts) + verdicts = ("killed", "crashed", "survived", "timeout") per_module = {} for mutant_id, corpus, _ in results: - entry = per_module.setdefault(by_id[mutant_id].module, [0, 0, 0]) - entry[{"killed": 0, "survived": 1, "timeout": 2}[corpus]] += 1 - print(f"{'module':<14} {'mutants':>7} {'killed':>7} {'survived':>8} {'timeout':>7}") - for module, (k, s, t) in sorted(per_module.items()): - print(f"{module:<14} {k + s + t:>7} {k:>7} {s:>8} {t:>7}") - total = len(results) - print( - f"{'total':<14} {total:>7} {killed:>7} {len(survivors):>8} {len(timeouts):>7}" - ) + counts = per_module.setdefault( + by_id[mutant_id].module, dict.fromkeys(verdicts, 0) + ) + counts[corpus] += 1 + totals = dict.fromkeys(verdicts, 0) + header = f"{'module':<14} {'mutants':>7}" + "".join(f" {v:>8}" for v in verdicts) + print(header) + for module, counts in sorted(per_module.items()): + row = f"{module:<14} {sum(counts.values()):>7}" + print(row + "".join(f" {counts[v]:>8}" for v in verdicts)) + for verdict in verdicts: + totals[verdict] += counts[verdict] + row = f"{'total':<14} {len(results):>7}" + print(row + "".join(f" {totals[v]:>8}" for v in verdicts)) if survivors: print("\nsurvivors:") From 19a725e51c23ede7f8095b5afcd603ff6d26a550 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 24 Aug 2026 07:09:45 -0700 Subject: [PATCH 5/7] tools: review fold-ins for the mutation harness Six findings from the multi-agent review of PR 64, each reproduced before fixing. The runner's malformed-case catch turned ValueError and KeyError escaping a mutated reference into vector verdicts, inflating kill counts. run_vectors gains MalformedCase, a VectorError subclass raised on that path, and the corpus driver classifies it as a crash. The driver stopped at the first failing file in alphabetical order, so a mutant's killed-versus-crashed verdict was an artifact of file ordering and every VM kill first paid the condition and validation files. It now continues past crashing files, lets a vector verdict win over a crash, and visits files cheapest first, in an order the baseline run measures. Under --tests the pytest oracle collected the mirror's own copy of the harness test, which mutated the already-mutated package, built nested mirrors, and manufactured false kills. That test and the corpus-runner test (a repeat of the run every survivor just passed) are excluded, and a generated conftest disables Hypothesis deadlines so parallel suites on a loaded machine cannot fail a latency check and report a false kill. The report keeps each run's output tail so a test kill names its failing test. Negating an if test whose condition is a bare equality-class comparison or a "not" duplicated the comparison swap or the "not" removal, one semantic mutant under two ids. Those negation sites are skipped, and AugAssign, IfExp, and While now get the operator swaps and test negations the docstring already claimed, closing the untouched augmented-assignment and expression-test sites the review counted, the message ledger's "+= 1" among them. Worker mirrors are removed at exit instead of accumulating in the temporary directory. Crashed and timed-out mutants are listed by site like survivors. __init__ is excluded from the inventory: its mutants would measure the export list, not the reference. --- python/tests/test_mutate.py | 60 +++++++-- tools/mutate.py | 260 +++++++++++++++++++++++++----------- tools/run_vectors.py | 11 +- 3 files changed, 243 insertions(+), 88 deletions(-) diff --git a/python/tests/test_mutate.py b/python/tests/test_mutate.py index 73d5a28..0958b7b 100644 --- a/python/tests/test_mutate.py +++ b/python/tests/test_mutate.py @@ -38,6 +38,30 @@ def test_constant_mutant_changes_exactly_one_constant(): assert changed == ["-QUOTE_COST = 20", "+QUOTE_COST = 21"] +def test_sites_cover_augassign_and_expression_tests_without_duplicates(): + source = ( + "x = 0\n" + "x += 1\n" + "if a == b:\n" + " pass\n" + "if a < b:\n" + " pass\n" + "while not c:\n" + " pass\n" + "y = 1 if d < 2 else 3\n" + ) + descriptions = [m.description for m in mutate.mutants_of("sample", source)] + # The augmented assignment is swapped like a binary operator. + assert "Add -> Sub" in descriptions + # `a == b` negated would duplicate the Eq -> NotEq swap, and + # `not c` negated would duplicate the `not` removal. `a < b` has + # no negation among the swaps, so its negation stays. + assert descriptions.count("if test negated") == 1 + assert "while test negated" not in descriptions + assert "ifexp test negated" in descriptions + assert "not removed" in descriptions + + def test_mirror_shares_data_and_copies_code(): root = mutate._build_mirror() try: @@ -45,19 +69,11 @@ def test_mirror_shares_data_and_copies_code(): assert (root / "puzzles").is_symlink() assert not (root / "python" / "bitlisp").is_symlink() assert (root / "tools" / "run_vectors.py").is_file() + assert (root / "python" / "tests" / "conftest.py").is_file() finally: shutil.rmtree(root) -def test_corpus_kills_a_broken_error_code_table(): - # Any mutant of the error-code table breaks the first vector file - # the corpus opens, so this integration test stays fast. - mutants = mutate.inventory({"errors"}) - broken = next(m for m in mutants if m.description != "raise deleted") - mutant_id, corpus, suite = mutate.evaluate(broken, timeout=120, tests=False) - assert (mutant_id, corpus, suite) == (broken.id, "killed", None) - - def test_verdict_separates_the_oracle_from_an_escaping_exception(): # Exit 0 survives, the oracle's own code kills, any other exit is # an exception escaping the reference and counts apart. @@ -66,9 +82,33 @@ def test_verdict_separates_the_oracle_from_an_escaping_exception(): verdicts = { code: mutate._run( root, [sys.executable, "-c", f"raise SystemExit({code})"], 30, {3} - ) + )[0] for code in (0, 3, 1) } finally: shutil.rmtree(root) assert verdicts == {0: "survived", 3: "killed", 1: "crashed"} + + +def test_corpus_judges_a_wrong_cost_and_a_poisoned_error_table_apart(): + # A wrong cost constant fails a vector's expectation: a kill the + # corpus earns. A poisoned error-code table makes every failure + # construction raise ValueError before any verdict: a crash. + costs = next( + m + for m in mutate.inventory({"costs"}) + if m.description == "20 -> 21" and "QUOTE_COST = 21" in m.source + ) + poisoned = next( + m for m in mutate.inventory({"errors"}) if m.description == "NotIn -> In" + ) + assert mutate.evaluate(costs, timeout=120, tests=False)[:3] == ( + costs.id, + "killed", + None, + ) + assert mutate.evaluate(poisoned, timeout=120, tests=False)[:3] == ( + poisoned.id, + "crashed", + None, + ) diff --git a/tools/mutate.py b/tools/mutate.py index 4b6d8f2..f55e6a4 100644 --- a/tools/mutate.py +++ b/tools/mutate.py @@ -7,25 +7,34 @@ corpus against each one. A mutant the corpus fails is killed. A mutant the corpus passes survived: either no vector pins the behavior that line implements, or the mutant is equivalent to the original. -Both readings need a human, so every survivor is reported with its -diff. A mutant that makes the reference raise something outside its -error taxonomy, so the runner stops on an escaping exception rather -than a vector's verdict, crashed: detected, but by Python rather than -by the corpus, and counted apart so the corpus's own coverage is not -overstated. +Both readings need a human, so survivors are reported with their +sites and any mutant's diff is printable with --only. A mutant that +makes the reference raise something outside its error taxonomy on +every file that does not pass, so no vector ever reaches a verdict, +crashed: detected, but by Python rather than by the corpus, and +counted apart so the corpus's own coverage is not overstated. When +one file crashes and another file's vector fails, the vector's +verdict wins and the mutant is killed. The corpus is the source of truth between sessions, so the corpus is the primary oracle. With --tests, each survivor is additionally run through the pytest suite (hypothesis invariants, oracle differentials, unit tests), separating survivors nothing catches from survivors the tests catch but the corpus does not. The first class is a gap in the -tests too. The second is a missing vector. - -Each worker runs in its own mirror of the repository under a temporary -directory (python/ and tools/ copied, vectors/ and puzzles/ linked), -so mutants never touch the checkout and workers never see each -other's edits. The unmutated tree must pass every oracle the mutants -face before any mutant runs. +tests too. The second is a missing vector. The mirror's copies of +this harness's own test and of the corpus-runner test are excluded: +the first would recurse into nested mirrors, the second repeats the +corpus run every survivor already passed. Hypothesis deadlines are +disabled for the pass, since parallel full suites on a loaded +machine would otherwise turn deadline overruns into false kills. + +Each worker runs in its own mirror of the repository under a +temporary directory (python/ and tools/ copied, vectors/ and puzzles/ +linked, removed when the worker exits), so mutants never touch the +checkout and workers never see each other's edits. The unmutated tree +must pass every oracle the mutants face before any mutant runs, and +that baseline run also times each vector file so every worker visits +the cheap files first. tools/mutate.py run everything, summary on stdout tools/mutate.py --list print the mutant inventory, run nothing @@ -34,12 +43,13 @@ tools/mutate.py --tests second pass over survivors tools/mutate.py --report out.json machine-readable results -Exit status: 0 when every mutant was killed or crashed, 1 when any -survived, 2 on a harness error (baseline failure, bad arguments). +Exit status: 0 when no mutant survived, 1 when any survived, 2 on a +harness error (baseline failure, bad arguments). """ import argparse import ast +import atexit import difflib import json import os @@ -67,6 +77,11 @@ ast.NotIn: ast.In, } +# The comparison operators whose swap is their own negation. Negating +# a test that is one bare comparison over these would duplicate the +# comparison mutant, so the negation site is skipped there. +NEGATION_SWAPS = (ast.Eq, ast.NotEq, ast.Is, ast.IsNot, ast.In, ast.NotIn) + BINOP_SWAPS = { ast.Add: ast.Sub, ast.Sub: ast.Add, @@ -96,6 +111,18 @@ def _op_name(op): return type(op).__name__ +def _negation_duplicates_another_site(test): + """True when negating this test would repeat a comparison swap (a + bare negation-swap comparison) or a `not` removal.""" + if isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not): + return True + return ( + isinstance(test, ast.Compare) + and len(test.ops) == 1 + and isinstance(test.ops[0], NEGATION_SWAPS) + ) + + class _Sites(ast.NodeVisitor): """Enumerates mutation sites in one module, in source order.""" @@ -105,6 +132,25 @@ def __init__(self): def _add(self, node, description, mutate): self.sites.append((node.lineno, len(self.sites), description, mutate)) + def _negate_test(self, node, what): + if _negation_duplicates_another_site(node.test): + return + test = node.test + + def mutate(): + node.test = ast.UnaryOp(op=ast.Not(), operand=test) + + self._add(node, f"{what} test negated", mutate) + + def _swap_op(self, node): + swap = BINOP_SWAPS.get(type(node.op)) + if swap is not None: + + def mutate(swap=swap): + node.op = swap() + + self._add(node, f"{_op_name(node.op)} -> {swap.__name__}", mutate) + def visit_Compare(self, node): for index, op in enumerate(node.ops): swap = COMPARE_SWAPS.get(type(op)) @@ -117,13 +163,11 @@ def mutate(index=index, swap=swap): self.generic_visit(node) def visit_BinOp(self, node): - swap = BINOP_SWAPS.get(type(node.op)) - if swap is not None: - - def mutate(swap=swap): - node.op = swap() + self._swap_op(node) + self.generic_visit(node) - self._add(node, f"{_op_name(node.op)} -> {swap.__name__}", mutate) + def visit_AugAssign(self, node): + self._swap_op(node) self.generic_visit(node) def visit_BoolOp(self, node): @@ -152,12 +196,15 @@ def mutate(delta=delta): self._add(node, f"{value} -> {value + delta}", mutate) def visit_If(self, node): - test = node.test + self._negate_test(node, "if") + self.generic_visit(node) - def mutate(): - node.test = ast.UnaryOp(op=ast.Not(), operand=test) + def visit_IfExp(self, node): + self._negate_test(node, "ifexp") + self.generic_visit(node) - self._add(node, "if test negated", mutate) + def visit_While(self, node): + self._negate_test(node, "while") self.generic_visit(node) def visit_Raise(self, node): @@ -202,10 +249,9 @@ def _not_sites(tree): return sites -def generate(module_path): - """Yields every mutant of one module, deterministically ordered.""" - source = module_path.read_text(encoding="utf-8") - name = module_path.stem +def mutants_of(name, source): + """Yields every mutant of one module's source, deterministically + ordered.""" baseline = ast.parse(source) visitor = _Sites() visitor.visit(baseline) @@ -238,10 +284,16 @@ def generate(module_path): ) +def generate(module_path): + yield from mutants_of(module_path.stem, module_path.read_text(encoding="utf-8")) + + def inventory(modules): + # __init__ only re-exports names, so its mutants would measure the + # export list, not the reference. mutants = [] for path in sorted(PACKAGE.glob("*.py")): - if modules and path.stem not in modules: + if path.stem == "__init__" or (modules and path.stem not in modules): continue mutants.extend(generate(path)) return mutants @@ -266,20 +318,40 @@ def mutant_diff(mutant): # mutant it is handed. _MIRROR = None -# The corpus driver exits with this code on a vector's verdict. Any -# other nonzero exit is an exception escaping the reference. + +# The corpus driver exits with VECTOR_FAILURE on a vector's verdict +# and CRASH when the reference raised outside its error taxonomy on +# some file and no vector verdict was reached. Timing mode (the +# MUTATE_TIME_FILES environment variable) runs every file on the +# unmutated tree and prints per-file seconds for the visit order. VECTOR_FAILURE = 3 +CRASH = 4 _CORPUS_DRIVER = f""" -VECTOR_FAILURE = {VECTOR_FAILURE} -import sys +import os, sys, time sys.path.insert(0, sys.argv[1]) import run_vectors -for path in run_vectors.discover(): +files = [run_vectors.REPO_ROOT / f for f in sys.argv[2:]] +if not files: + files = list(run_vectors.discover()) +timing = bool(os.environ.get("MUTATE_TIME_FILES")) +crashed = False +for path in files: + started = time.monotonic() try: run_vectors.run_file(path) + except run_vectors.MalformedCase as exc: + print(exc) + crashed = True except run_vectors.VectorError as exc: print(exc) - sys.exit(VECTOR_FAILURE) + sys.exit({VECTOR_FAILURE}) + except Exception as exc: + print(f"{{path}}: {{exc!r}}") + crashed = True + if timing: + rel = path.relative_to(run_vectors.REPO_ROOT) + print(f"{{time.monotonic() - started:.3f}}\\t{{rel}}", file=sys.stderr) +sys.exit({CRASH} if crashed else 0) """ @@ -291,6 +363,16 @@ def _build_mirror(): shutil.copy(REPO_ROOT / "pyproject.toml", root / "pyproject.toml") for shared in ("vectors", "puzzles"): os.symlink(REPO_ROOT / shared, root / shared) + # Hypothesis's default 200 ms deadline is a latency check, not an + # oracle: parallel full suites on a loaded machine would fail it + # nondeterministically and report false kills. + (root / "python" / "tests" / "conftest.py").write_text( + "from hypothesis import settings\n" + 'settings.register_profile("mutate", deadline=None)\n' + 'settings.load_profile("mutate")\n', + encoding="utf-8", + ) + atexit.register(shutil.rmtree, root, ignore_errors=True) return root @@ -313,11 +395,13 @@ def _restore(root, module): shutil.copy(PACKAGE / f"{module}.py", root / "python" / "bitlisp" / f"{module}.py") -def _run(root, argv, timeout, verdict_codes): - """Runs argv in the mirror. Returns 'survived' on exit 0, 'killed' on - an exit code in verdict_codes (the oracle judged the mutant), - 'crashed' on any other exit, or 'timeout'.""" +def _run(root, argv, timeout, verdict_codes, env_extra=None): + """Runs argv in the mirror. Returns (verdict, detail): 'survived' + on exit 0, 'killed' on an exit code in verdict_codes (the oracle + judged the mutant), 'crashed' on any other exit, or 'timeout'. + detail is the output's tail, kept for the report.""" env = dict(os.environ, PYTHONPATH=str(root / "python"), PYTHONDONTWRITEBYTECODE="1") + env.update(env_extra or {}) try: proc = subprocess.run( argv, @@ -328,58 +412,84 @@ def _run(root, argv, timeout, verdict_codes): timeout=timeout, ) except subprocess.TimeoutExpired: - return "timeout" + return "timeout", "" + detail = (proc.stdout + proc.stderr)[-2000:] if proc.returncode == 0: - return "survived" - return "killed" if proc.returncode in verdict_codes else "crashed" + return "survived", detail + return ("killed" if proc.returncode in verdict_codes else "crashed"), detail -def run_corpus(root, timeout): - argv = [sys.executable, "-c", _CORPUS_DRIVER, str(root / "tools")] +def run_corpus(root, timeout, order=()): + argv = [sys.executable, "-c", _CORPUS_DRIVER, str(root / "tools"), *order] return _run(root, argv, timeout, {VECTOR_FAILURE}) def run_tests(root, timeout): + tests = root / "python" / "tests" argv = [ sys.executable, "-m", "pytest", - str(root / "python" / "tests"), + str(tests), + # The harness's own test would recurse into nested mirrors, + # and the corpus-runner test repeats the corpus run every + # survivor already passed. + f"--ignore={tests / 'test_mutate.py'}", + f"--ignore={tests / 'test_vectors.py'}", "-q", "-x", "-p", "no:cacheprovider", ] # pytest exits 1 on failing tests. Its other codes (interrupted, - # internal error, usage error, no tests collected) judge nothing. + # collection or internal error, usage error, no tests) judge + # nothing. return _run(root, argv, timeout, {1}) -def evaluate(mutant, timeout, tests): +def evaluate(mutant, timeout, tests, order=()): root = _mirror() _install(root, mutant) try: - corpus = run_corpus(root, timeout) + corpus, detail = run_corpus(root, timeout, order) suite = None if tests and corpus == "survived": - suite = run_tests(root, timeout * 10) + suite, detail = run_tests(root, timeout * 10) finally: _restore(root, mutant.module) - return mutant.id, corpus, suite + return mutant.id, corpus, suite, detail def _evaluate_star(args): return evaluate(*args) -def baseline_passes(timeout, tests): - """The unmutated tree must pass every oracle the mutants face, - else a broken mirror would report every mutant killed.""" +def baseline_order(timeout, tests): + """Runs the unmutated tree against every oracle the mutants will + face. Returns the vector files ordered cheapest first, or None + when the baseline fails: a broken mirror would otherwise report + every mutant killed.""" root = _build_mirror() try: - if run_corpus(root, timeout) != "survived": - return False - return not tests or run_tests(root, timeout * 10) == "survived" + verdict, detail = _run( + root, + [sys.executable, "-c", _CORPUS_DRIVER, str(root / "tools")], + timeout * 10, + {VECTOR_FAILURE}, + env_extra={"MUTATE_TIME_FILES": "1"}, + ) + if verdict != "survived": + return None + timed = [] + for line in detail.splitlines(): + seconds, _, rel = line.partition("\t") + try: + timed.append((float(seconds), rel)) + except ValueError: + continue + if tests and run_tests(root, timeout * 10)[0] != "survived": + return None + return tuple(rel for _, rel in sorted(timed)) finally: shutil.rmtree(root) @@ -431,7 +541,8 @@ def main(): print(f"{len(mutants)} mutant(s)") return 0 - if not baseline_passes(args.timeout, args.tests): + order = baseline_order(args.timeout, args.tests) + if order is None: print( "the unmutated tree fails its oracle, refusing to mutate", file=sys.stderr ) @@ -439,28 +550,24 @@ def main(): by_id = {m.id: m for m in mutants} results = [] - work = [(m, args.timeout, args.tests) for m in mutants] + work = [(m, args.timeout, args.tests, order) for m in mutants] with ProcessPoolExecutor(max_workers=args.jobs) as pool: - for done, (mutant_id, corpus, suite) in enumerate( + for done, (mutant_id, corpus, suite, detail) in enumerate( pool.map(_evaluate_star, work, chunksize=4), start=1 ): - results.append((mutant_id, corpus, suite)) + results.append((mutant_id, corpus, suite, detail)) if done % 50 == 0 or done == len(work): print(f"{done}/{len(work)}", file=sys.stderr) - survivors = [r for r in results if r[1] == "survived"] - timeouts = [r for r in results if r[1] == "timeout"] - verdicts = ("killed", "crashed", "survived", "timeout") per_module = {} - for mutant_id, corpus, _ in results: + for mutant_id, corpus, _, _ in results: counts = per_module.setdefault( by_id[mutant_id].module, dict.fromkeys(verdicts, 0) ) counts[corpus] += 1 totals = dict.fromkeys(verdicts, 0) - header = f"{'module':<14} {'mutants':>7}" + "".join(f" {v:>8}" for v in verdicts) - print(header) + print(f"{'module':<14} {'mutants':>7}" + "".join(f" {v:>8}" for v in verdicts)) for module, counts in sorted(per_module.items()): row = f"{module:<14} {sum(counts.values()):>7}" print(row + "".join(f" {counts[v]:>8}" for v in verdicts)) @@ -469,17 +576,15 @@ def main(): row = f"{'total':<14} {len(results):>7}" print(row + "".join(f" {totals[v]:>8}" for v in verdicts)) - if survivors: - print("\nsurvivors:") - for mutant_id, _, suite in survivors: + for verdict in ("survived", "crashed", "timeout"): + listed = [r for r in results if r[1] == verdict] + if not listed: + continue + print(f"\n{verdict}:") + for mutant_id, _, suite, _ in listed: m = by_id[mutant_id] tag = "" if suite is None else f" [tests: {suite}]" print(f" {m.id:<20} {m.module}.py:{m.line:<5} {m.description}{tag}") - if timeouts: - print("\ntimeouts:") - for mutant_id, _, _ in timeouts: - m = by_id[mutant_id] - print(f" {m.id:<20} {m.module}.py:{m.line:<5} {m.description}") if args.report: report = [ @@ -487,15 +592,16 @@ def main(): asdict(by_id[mutant_id]), corpus=corpus, tests=suite, + detail=detail, diff=mutant_diff(by_id[mutant_id]), ) - for mutant_id, corpus, suite in results + for mutant_id, corpus, suite, detail in results ] for entry in report: del entry["source"] args.report.write_text(json.dumps(report, indent=1), encoding="utf-8") - return 1 if survivors else 0 + return 1 if totals["survived"] else 0 if __name__ == "__main__": diff --git a/tools/run_vectors.py b/tools/run_vectors.py index a9f37e2..197608c 100755 --- a/tools/run_vectors.py +++ b/tools/run_vectors.py @@ -38,6 +38,13 @@ class VectorError(Exception): """A vector file is malformed or a case failed.""" +class MalformedCase(VectorError): + """A case raised outside the error taxonomy: a malformed vector, + or an implementation raising something no vector can expect. + Distinct so tooling that judges implementations by vector verdict + (the mutation harness) can tell the two apart.""" + + def validate_envelope(obj, path=""): """Checks the envelope shape. Returns the validated object. @@ -412,7 +419,9 @@ def run_suite(envelope, path): except VectorError as exc: raise VectorError(f"{path}: {name}: {exc}") from None except (KeyError, ValueError) as exc: - raise VectorError(f"{path}: {name}: malformed case: {exc!r}") from None + raise MalformedCase( + f"{path}: {name}: malformed case: {exc!r}" + ) from None return run_suite From dac86bafdb21f0c9ff406706b788c4d807b55aea Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 24 Aug 2026 07:09:45 -0700 Subject: [PATCH 6/7] vectors: two review corrections The two 253-byte seal cases are removed. The review ran the mutation both ways: the pre-existing seal boundary case and the seal unit suite already pin the 253 side of the compact-size boundary, and with both cases deleted every _compact_size mutant is still killed, so only the 252 case, which is kept, closes a gap. The largest case in the corpus pinned nothing. The two path-cost cases are renamed from interior to trailing zero bytes: programs 0100 and 000100 place the zero byte at the tail, and the old names claimed a position the bytes do not have. Case content is unchanged. --- vectors/validation/seals.json | 1064 --------------------------------- vectors/vm/paths.json | 4 +- 2 files changed, 2 insertions(+), 1066 deletions(-) diff --git a/vectors/validation/seals.json b/vectors/validation/seals.json index e6f7f0b..40e18e8 100644 --- a/vectors/validation/seals.json +++ b/vectors/validation/seals.json @@ -765,1070 +765,6 @@ "expect": { "valid": true } - }, - { - "name": "seal_outputs_script_253_bytes_three_byte_length", - "tx": { - "version": 2, - "locktime": 0, - "inputs": [ - { - "txid": "1111111111111111111111111111111111111111111111111111111111111111", - "index": 0, - "script_pubkey": "5120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "amount": 1000, - "conditions": "ffff61ffa0430b99e7b40d8989ab44a9c14f35ff4eff3a60a9f91e4d519b708fd59cdb96e58080", - "tapleaf": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", - "merkle_root": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", - "internal_key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" - } - ], - "outputs": [ - { - "script_pubkey": "6acccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "amount": 400 - } - ] - }, - "expect": { - "valid": true - } - }, - { - "name": "seal_txid_253_outputs_three_byte_count", - "tx": { - "version": 2, - "locktime": 0, - "inputs": [ - { - "txid": "1111111111111111111111111111111111111111111111111111111111111111", - "index": 0, - "script_pubkey": "5120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "amount": 1000, - "conditions": "ffff60ffa047f77dff9478e4755b52291963d8800bf6167c4e9c9e6c6e76d02488c9456df78080", - "tapleaf": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", - "merkle_root": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", - "internal_key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c" - } - ], - "outputs": [ - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - }, - { - "script_pubkey": "6a", - "amount": 1 - } - ] - }, - "expect": { - "valid": true - } } ] } diff --git a/vectors/vm/paths.json b/vectors/vm/paths.json index 19e7a11..b1891ba 100644 --- a/vectors/vm/paths.json +++ b/vectors/vm/paths.json @@ -156,7 +156,7 @@ } }, { - "name": "path_interior_zero_byte", + "name": "path_trailing_zero_byte", "program": "820100", "env": "ffffffffffffffff0a8080808080808080", "expect": { @@ -165,7 +165,7 @@ } }, { - "name": "path_leading_and_interior_zero_bytes", + "name": "path_leading_and_trailing_zero_bytes", "program": "83000100", "env": "ffffffffffffffff0a8080808080808080", "expect": { From 1c96d9a513fab0e868a61d95a8ea416e11f1318c Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 24 Aug 2026 08:02:15 -0700 Subject: [PATCH 7/7] docs: the final pass numbers with crashes counted apart The full re-run after the review fold-ins counts 1,651 mutants: 1,241 killed by the corpus, 225 crashed (detected by Python before any verdict, no kill credit), 180 survived, 5 timed out. The pass table gains the crashed column, and the numbers paragraph is rewritten from the new run: the pytest suite kills 52 of the 180 survivors and 128 survive both oracles. Three former test kills were artifacts of the mirror's own harness test mutating the already-mutated package and now survive genuinely: the error-code guard in errors.py, a frozen flag in conditions.py, and sha256tree's argument index in operators.py. Each already sits in an accepted class. The one new survivor, from the newly mutated expression tests, is the taproot-versus-scriptpubkey name choice in validation.py's unsatisfied-scriptpubkey message. It only names a condition in a message and joins the same-code class beside the name table index. --- docs/mutation-triage.md | 99 ++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/docs/mutation-triage.md b/docs/mutation-triage.md index 940017e..243eac7 100644 --- a/docs/mutation-triage.md +++ b/docs/mutation-triage.md @@ -21,6 +21,11 @@ negated, each `not` removed, each `raise` deleted, and `break` and `continue` exchanged. Each mutant runs the whole corpus in a private mirror of the tree. A mutant the corpus fails is killed. One the corpus passes survived and is triaged by hand into a class below. +One that makes the reference raise outside its error taxonomy, so +the runner stops on an escaping exception before any vector's +verdict, crashed: detected by Python rather than by the corpus, and +counted apart so the corpus's coverage is not overstated by kills it +did not earn. With `--tests`, every corpus survivor also runs the pytest suite (hypothesis invariants, oracle differentials, unit tests), which separates survivors nothing catches from survivors the tests catch @@ -29,15 +34,16 @@ but the corpus does not. .venv/bin/python tools/mutate.py --tests --report mutants.json The run refuses to start unless the unmutated tree passes every -oracle the mutants face. Timeouts count as kills: a mutant that -hangs is a mutant the budget eventually rejects, at a cost in wall -clock the harness will not pay. +oracle the mutants face. A timeout is the same kind of detection: a +mutant that hangs is a mutant the budget eventually rejects, at a +cost in wall clock the harness will not pay, so it too is counted +apart from the kills. ## Survivor classes -A survivor lands in exactly one class. The first five are accepted -without a vector, each for a stated reason. The sixth is the finding -the harness exists for. +Each survivor is triaged into one class. The first five are +accepted without a vector, each for a stated reason. The sixth is +the finding the harness exists for. | class | meaning | accepted because | | --- | --- | --- | @@ -57,48 +63,55 @@ pinned by `--tests`. ## Pass of 2026-08-23 Run against `main` at PR 60 (`7eee46f`) with `--tests`, the corpus -at 1,151 cases after this pass's nineteen vectors. - -| module | mutants | killed | survived | timeout | -| --- | --- | --- | --- | --- | -| conditions | 481 | 442 | 39 | 0 | -| costs | 114 | 114 | 0 | 0 | -| errors | 3 | 2 | 1 | 0 | -| machine | 80 | 75 | 5 | 0 | -| operators | 379 | 361 | 17 | 1 | -| secp256k1 | 153 | 137 | 15 | 1 | -| serialize | 160 | 125 | 34 | 1 | -| sexp | 37 | 37 | 0 | 0 | -| tx | 165 | 104 | 61 | 0 | -| validation | 161 | 154 | 7 | 0 | -| total | 1,733 | 1,551 | 179 | 3 | - -The numbers are the state after the nineteen vectors below landed. -The first pass, on the tree before PR 60 merged, had 195 survivors -out of 1,720 mutants. Of the 179 remaining, the -pytest suite kills 55: 29 of the 61 model preconditions in `tx.py`, +at 1,149 cases after this pass's seventeen vectors. + +| module | mutants | killed | crashed | survived | timeout | +| --- | --- | --- | --- | --- | --- | +| conditions | 426 | 330 | 57 | 39 | 0 | +| costs | 114 | 114 | 0 | 0 | 0 | +| errors | 2 | 0 | 1 | 1 | 0 | +| machine | 79 | 58 | 16 | 5 | 0 | +| operators | 369 | 300 | 51 | 17 | 1 | +| secp256k1 | 148 | 104 | 27 | 15 | 2 | +| serialize | 166 | 101 | 29 | 34 | 2 | +| sexp | 35 | 26 | 9 | 0 | 0 | +| tx | 168 | 92 | 15 | 61 | 0 | +| validation | 144 | 116 | 20 | 8 | 0 | +| total | 1,651 | 1,241 | 225 | 180 | 5 | + +The numbers are the state after the vectors below landed and after +the review fold-ins reshaped the inventory: crashes and timeouts +told apart from kills, the doubled negations deduplicated, the +augmented-assignment, expression-test, and while sites added, and +`__init__` excluded. The first pass, on the tree before PR 60 +merged, had 195 survivors out of 1,720 mutants under the old +counting. The 225 crashes are dominated by deleted raises and +shifted indices that Python detects before any verdict, and the +corpus takes no kill credit for them. Of the 180 survivors, the +pytest suite kills 52: 29 of the 61 model preconditions in `tx.py`, ten of the fifteen in `secp256k1.py` (the group order moved by one, the width guards, the point-at-infinity branch), nine in -`serialize.py` (the length-form boundary at 2^20, the `bytes`-only -type check, two truncation checks), the error-code guard, -`condition_cost`, and a few `[0]` indices and flags elsewhere. The -other 124 survive both, the five in `machine.py` and all seven in -`validation.py` among them. All 179 fall into the accepted classes -below. The -three timeouts are mutants that loop until the budget rejects them: -`if` returning its else branch in both cases, a scalar multiplication -that never shifts its scalar, a deserializer that never advances. +`serialize.py` (length-form table constants, the floor at 2^20 +among them, the `bytes`-only type check, the truncated-atom check), +the `name` table index and the reserved branch of `condition_cost` +in `conditions.py`, and one of `substr`'s negative-index checks. +The other 128 survive both, the five in `machine.py` and all eight +in `validation.py` among them. All 180 fall into the accepted +classes below. The five timeouts are mutants that loop until the +budget rejects them: the `if` operator returning one branch in both +cases, the scalar multiplication shifting its scalar the wrong way +or by zero, and the deserializer stepping backward or not at all. ### Gaps found, vectors added -Nineteen cases, each a behavior the spec states that no vector +Seventeen cases, each a behavior the spec states that no vector exercised. All pass the reference, the two vm path cases were -cross-checked against the consensus oracle, and the three seal cases +cross-checked against the consensus oracle, and the seal case against the vendored Bitcoin Core framework. | site | mutant that survived | vector | spec | | --- | --- | --- | --- | -| path cost, leading zero bytes | `break` to `continue` in the leading-zero count: an interior zero byte was counted as leading | `vm/paths.json` `path_interior_zero_byte`, `path_leading_and_interior_zero_bytes` | VM.md section 3.1 | +| path cost, leading zero bytes | `break` to `continue` in the leading-zero count: a zero byte after a nonzero byte was counted as leading | `vm/paths.json` `path_trailing_zero_byte`, `path_leading_and_trailing_zero_bytes` | VM.md section 3.1 | | one-byte atom in the long form | `<= 0x7F` to `< 0x7F` and `0x7F` to `0x7E`: `0x81 0x7F` accepted as canonical | `vm/serialize.json` `nonminimal_one_byte_atom_7f` | VM.md section 2 (D5) | | invalid prefix byte | `>= 0xFC` to `> 0xFC`: a lone `0xFC` fell through every length form | `vm/serialize.json` `lone_prefix_fc` | VM.md section 2 (D5) | | three-byte length form floor | floor `0x2000` to `0x1FFF`: a length of 8,191 in the three-byte form accepted as minimal | `vm/serialize.json` `nonminimal_length_e0_at_8191` | VM.md section 2 (D5) | @@ -106,10 +119,10 @@ against the vendored Bitcoin Core framework. | reserved declared cost | `cost < 0` to `cost < -1`: a declared cost of exactly -1 reported as `reserved_cost_too_low` instead of `bad_condition_arg` | `conditions/encoding.json` `reserved_cost_minus_one` | CONDITIONS.md section 1, VALIDATION.md rule 6 | | ASSERT_MY_SCRIPTPUBKEY and ASSERT_MY_AMOUNT arity | the arity raise deleted: two operands accepted, the second ignored | `conditions/self-asserts.json` `scriptpubkey_arity_zero`, `scriptpubkey_arity_two`, `amount_arity_zero`, `amount_arity_two` | CONDITIONS.md self asserts | | specifier field shape | the atom check on a non-amount specifier field deleted: a pair carried into the ledger | `conditions/messages.json` `assure_script_specifier_pair` | CONDITIONS.md message family | -| ASSERT_ANNOUNCEMENT arity | the arity raise deleted: an empty list crashed instead of reporting `bad_condition_arity` | `conditions/messages.json` `assert_announcement_arity_zero` | CONDITIONS.md message family | +| ASSERT_ANNOUNCEMENT arity | the arity raise deleted: an empty list crashed instead of reporting `bad_condition_arity`. The vector pins the error code, and the harness reports the mutant crashed rather than killed, since Python detects it before any verdict | `conditions/messages.json` `assert_announcement_arity_zero` | CONDITIONS.md message family | | composed specifier operand order | `continue` to `break` after an amount field: the identity fields after it never parsed | `conditions/messages.json` `assure_amount_tapleaf_specifier_parses` | CONDITIONS.md message family | -| specifier amount domain | `0 <= value` to `0 < value`: a zero-amount prevout unaddressable by amount | `validation/messages.json` `amount_specifier_over_zero_amount_input_balances` | VALIDATION.md rule 3 (C9) | -| compact-size boundary in the txid and outputs hash | `n < 0xFD` to `n < 0xFC`: a 252-byte script or count encoded in the three-byte form | `validation/seals.json` `seal_outputs_script_252_bytes_one_byte_length`, `seal_outputs_script_253_bytes_three_byte_length`, `seal_txid_253_outputs_three_byte_count` | CONDITIONS.md seals | +| specifier amount domain | `0 <= value` to `0 < value`: a zero-amount prevout unaddressable by amount | `validation/messages.json` `amount_specifier_over_zero_amount_input_balances` | VALIDATION.md rule 3, divergence C9 in the condition record | +| compact-size boundary in the txid and outputs hash | `n < 0xFD` to `n < 0xFC`: a 252-byte script encoded in the three-byte form | `validation/seals.json` `seal_outputs_script_252_bytes_one_byte_length`. The 253 side was already pinned by the existing boundary case and the seal unit suite, and two review-added 253 cases were removed as pinning nothing | VALIDATION.md transaction view (the txid and outputs-hash serializations) | ### Survivors accepted @@ -146,7 +159,9 @@ checks shadowed by width checks in the condition parsers (a pair has length two), the first arity check of the two variadic message parsers (the mode-derived count check reports the same code), the `(empty)` fallbacks in error messages, and the `name` property's -table index, which only names a condition in a message. +table index and the taproot-versus-scriptpubkey choice in the +unsatisfied-scriptpubkey message, each of which only names a +condition in a message. **Model precondition.** Every constructor check in `tx.py`: field ranges, byte types, the non-empty input and output tuples, distinct