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/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..243eac7 --- /dev/null +++ b/docs/mutation-triage.md @@ -0,0 +1,186 @@ +# 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. +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 +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. 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 + +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 | +| --- | --- | --- | +| 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,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` (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 + +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 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: 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) | +| 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`. 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, 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 + +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 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 +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. diff --git a/python/tests/test_mutate.py b/python/tests/test_mutate.py new file mode 100644 index 0000000..0958b7b --- /dev/null +++ b/python/tests/test_mutate.py @@ -0,0 +1,114 @@ +"""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_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: + 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() + assert (root / "python" / "tests" / "conftest.py").is_file() + finally: + shutil.rmtree(root) + + +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} + )[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 new file mode 100644 index 0000000..f55e6a4 --- /dev/null +++ b/tools/mutate.py @@ -0,0 +1,608 @@ +#!/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 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. 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 + 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 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 +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, +} + +# 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, + 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__ + + +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.""" + + def __init__(self): + self.sites = [] + + 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)) + 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): + self._swap_op(node) + self.generic_visit(node) + + def visit_AugAssign(self, node): + self._swap_op(node) + 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): + self._negate_test(node, "if") + self.generic_visit(node) + + def visit_IfExp(self, node): + self._negate_test(node, "ifexp") + self.generic_visit(node) + + def visit_While(self, node): + self._negate_test(node, "while") + 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 mutants_of(name, source): + """Yields every mutant of one module's source, deterministically + ordered.""" + 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 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 path.stem == "__init__" or (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 + +# 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""" +import os, sys, time +sys.path.insert(0, sys.argv[1]) +import run_vectors +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}) + 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) +""" + + +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) + # 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 + + +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, 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, + cwd=root, + env=env, + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + return "timeout", "" + detail = (proc.stdout + proc.stderr)[-2000:] + if proc.returncode == 0: + return "survived", detail + return ("killed" if proc.returncode in verdict_codes else "crashed"), detail + + +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(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, + # collection or internal error, usage error, no tests) judge + # nothing. + return _run(root, argv, timeout, {1}) + + +def evaluate(mutant, timeout, tests, order=()): + root = _mirror() + _install(root, mutant) + try: + corpus, detail = run_corpus(root, timeout, order) + suite = None + if tests and corpus == "survived": + suite, detail = run_tests(root, timeout * 10) + finally: + _restore(root, mutant.module) + return mutant.id, corpus, suite, detail + + +def _evaluate_star(args): + return evaluate(*args) + + +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: + 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) + + +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 + + order = baseline_order(args.timeout, args.tests) + if order is None: + 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, order) for m in mutants] + with ProcessPoolExecutor(max_workers=args.jobs) as pool: + for done, (mutant_id, corpus, suite, detail) in enumerate( + pool.map(_evaluate_star, work, chunksize=4), start=1 + ): + results.append((mutant_id, corpus, suite, detail)) + if done % 50 == 0 or done == len(work): + print(f"{done}/{len(work)}", file=sys.stderr) + + verdicts = ("killed", "crashed", "survived", "timeout") + per_module = {} + 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) + 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)) + 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)) + + 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 args.report: + report = [ + dict( + asdict(by_id[mutant_id]), + corpus=corpus, + tests=suite, + detail=detail, + diff=mutant_diff(by_id[mutant_id]), + ) + 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 totals["survived"] else 0 + + +if __name__ == "__main__": + sys.exit(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 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..40e18e8 100644 --- a/vectors/validation/seals.json +++ b/vectors/validation/seals.json @@ -737,6 +737,34 @@ "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 + } } ] } diff --git a/vectors/vm/paths.json b/vectors/vm/paths.json index ba97dc3..b1891ba 100644 --- a/vectors/vm/paths.json +++ b/vectors/vm/paths.json @@ -154,6 +154,24 @@ "result": "80", "cost": 44 } + }, + { + "name": "path_trailing_zero_byte", + "program": "820100", + "env": "ffffffffffffffff0a8080808080808080", + "expect": { + "result": "0a", + "cost": 76 + } + }, + { + "name": "path_leading_and_trailing_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" + } } ] }