diff --git a/.github/workflows/developer-onboarding.yml b/.github/workflows/developer-onboarding.yml new file mode 100644 index 0000000..d570fd8 --- /dev/null +++ b/.github/workflows/developer-onboarding.yml @@ -0,0 +1,81 @@ +name: Real developer onboarding and consumer gates +on: + push: + paths: + - 'examples/range-client/**' + - 'scripts/developer_walkthrough.py' + - 'tests/test_developer_walkthrough.py' + - '.github/workflows/developer-onboarding.yml' + - 'shadecheck/**' + - 'action.yml' + - 'pyproject.toml' + - 'docs/GETTING_STARTED.md' + workflow_dispatch: +permissions: + contents: read +jobs: + onboarding: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Build wheel and install into a clean consumer environment + run: | + python -m pip wheel . --wheel-dir "$RUNNER_TEMP/shadecheck-wheels" + python -m venv "$RUNNER_TEMP/shadecheck-consumer" + "$RUNNER_TEMP/shadecheck-consumer/bin/python" -m pip install --no-index --find-links "$RUNNER_TEMP/shadecheck-wheels" shadecheck + - name: Check clean consumer prerequisites + run: '"$RUNNER_TEMP/shadecheck-consumer/bin/python" -m shadecheck.cli doctor --json' + - name: Execute external client baseline and changed behavior on real blocks + run: '"$RUNNER_TEMP/shadecheck-consumer/bin/python" scripts/developer_walkthrough.py --output out/onboarding-ci' + - name: Recompute original evidence without new RPCs + run: '"$RUNNER_TEMP/shadecheck-consumer/bin/python" scripts/developer_walkthrough.py --verify out/onboarding-ci' + - name: Compliant consumer gate must pass + uses: ./ + with: + events: out/onboarding-ci/unchanged-run/events.jsonl + config: out/onboarding-ci/policy.json + baseline: out/onboarding-ci/baseline.json + rules: SC-001 + output-dir: out/onboarding-action-pass + - name: Changed consumer gate must fail + uses: ./ + id: changed + continue-on-error: true + with: + events: out/onboarding-ci/changed-run/events.jsonl + config: out/onboarding-ci/policy.json + baseline: out/onboarding-ci/baseline.json + rules: SC-001 + output-dir: out/onboarding-action-fail + - name: Require a real policy failure and regression, not an execution error + env: + ACTUAL_OUTCOME: ${{ steps.changed.outcome }} + ACTUAL_STATUS: ${{ steps.changed.outputs.status }} + ACTUAL_EXIT: ${{ steps.changed.outputs.exit-code }} + run: | + test "$ACTUAL_OUTCOME" = failure + test "$ACTUAL_STATUS" = FAIL + test "$ACTUAL_EXIT" = 1 + python - <<'PY' + import json + from pathlib import Path + r=json.loads(Path('out/onboarding-action-fail/report.json').read_text()) + assert {f['rule_id'] for f in r['findings']} == {'SC-001','SC-005'} + assert r['baseline_comparison']['regressions'] == 1 + PY + - uses: actions/upload-artifact@v4 + if: always() + with: + name: real-developer-onboarding + path: | + out/onboarding-ci/ + out/onboarding-action-pass/ + out/onboarding-action-fail/ + retention-days: 30 + - name: Stop dedicated regression backend, preserving local data + if: always() + run: docker compose -f integration/compose.yml down diff --git a/README.md b/README.md index b878f9b..d7e9afc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,9 @@ A transaction can be correctly shielded while application requests still expose | What you want to do | Where to go | | --- | --- | | Explore findings immediately | [Open the public evidence viewer](https://shade-check-nu.vercel.app/) — saved real regtest reports; no setup required | -| Execute fresh tests | [Run locally](#run-the-real-system) with Python and Docker | +| Try the first developer workflow | [Getting started](docs/GETTING_STARTED.md): saved proof or fresh baseline → unchanged → changed-request test | +| Connect your client and enforce CI | [Observer, requirement and consumer integration](docs/CLIENT_INTEGRATION.md) | +| Execute the full five-family suite | [Run locally](#run-the-real-system) with Python and Docker | | Check what was actually verified | [Acceptance audit](docs/acceptance.md) and [validation record](docs/RELEASE_VALIDATION.md) | | Inspect an independent client integration | [Current funded Zingo / Zebra / Zaino test](docs/modern-wallet.md) and [original evidence](examples/modern/README.md) | | Understand the limits | [Threat model](docs/threat-model.md), [rule applicability](docs/rule-applicability.md), and [submission review](docs/JUDGE_REVIEW.md) | @@ -63,6 +65,10 @@ Fresh recordings use per-run keyed session identifiers and omit arbitrary broadc Select requirements that match your actual observer and workflow. A finding is not automatically a newly discovered vulnerability. See [rule applicability and developer actions](docs/rule-applicability.md). +## First developer workflow + +Follow [getting started](docs/GETTING_STARTED.md) for Windows and Bash commands, expected outputs and troubleshooting. A separate example client retrieves real completed block ranges through the observer. The walkthrough validates a compliant baseline, a fresh unchanged run and an actual changed request, then asserts the resulting policy and regression exits. The [real onboarding CI](.github/workflows/developer-onboarding.yml) installs a wheel into a clean environment and tests the consumer Action against those traces. [Real onboarding CI](https://github.com/0xaje/ShadeCheck-/actions/runs/40103273814) passed; its [original evidence](examples/range-client/README.md#original-recorded-onboarding-proof) is committed for offline inspection. This is automated clean-environment validation, not a claim of independent user adoption. + ## Run the real system The five-family acceptance suite uses a frozen zcashd/lightwalletd regression backend. It is a controlled test target, not a recommendation for a new production wallet backend. The separate [current Zebra/Zaino wallet integration](docs/modern-wallet.md) validates its own documented coverage. diff --git a/docs/CLIENT_INTEGRATION.md b/docs/CLIENT_INTEGRATION.md new file mode 100644 index 0000000..e08e317 --- /dev/null +++ b/docs/CLIENT_INTEGRATION.md @@ -0,0 +1,98 @@ +# Connect a controlled client and choose a useful CI requirement + +First complete the [getting-started walkthrough](GETTING_STARTED.md). The small [range client](../examples/range-client/client.py) is an integration example, not a wallet. Existing wallet integrations require their own supported RPCs, network parameters and reproducible workflow. + +## Write the requirement before enabling a gate + +Record four things in your integration documentation: + +1. Observer: which service can see the requests? +2. Promise: which behavior should that observer not see, or which explicit batching policy should it see? +3. Exercise: which real client workflow provides the required rule coverage? +4. Action: what code/configuration would a developer investigate if it fails? + +For this example, the observer serves historical compact blocks; the application promises fixed-size aligned batches for a completed range. A narrower request is a scheduling-policy regression, not proof of wallet ownership or a newly discovered Zcash vulnerability. + +A direct broadcasting service inherently receives submitted bytes. SC-002 is appropriate only when that visibility matters to the declared trust model. Hiding a label or removing observation coverage does not establish a privacy improvement. + +## Capture actual traffic + +Start the dedicated local backend with `python scripts/regtest.py up`. In terminal A: + +```bash +python -m shadecheck.cli observe --upstream 127.0.0.1:9067 --plaintext-upstream --port 9068 --output out/my-client/events.jsonl +``` + +Keep that terminal open. The observer listens on loopback. Plaintext is explicit here for controlled local regtest; use the supported TLS upstream configuration for another approved test environment. No spending key is sent to ShadeCheck. + +In terminal B, run your real client against `127.0.0.1:9068`. The example below requests historical heights 80–99, which the initialized local backend must already contain: + +```bash +python examples/range-client/client.py --server 127.0.0.1:9068 --start 80 --end 99 +``` + +Wait for the client to complete, then stop terminal A with Ctrl+C. Use a new event path for each run; the recorder refuses to overwrite evidence. The [automated walkthrough](GETTING_STARTED.md) also independently compares every returned block hash with the node. + +Create a policy file named `my-policy.json`: + +```json +{"schema_version":1,"sync":{"chunk_size":20,"alignment_height":0}} +``` + +Evaluate only the rule exercised here: + +```bash +python -m shadecheck.cli test --events out/my-client/events.jsonl --rules SC-001 --config my-policy.json --policy strict --output out/my-client/report.json +python -m shadecheck.cli report --input out/my-client/report.json --format html --output out/my-client/report.html +``` + +The complete aligned example should return exit 0. A complete request for heights 83–89 should produce SC-001 and exit 1 under the same policy. Neither result establishes anonymity. Missing coverage, invalid input or execution errors require investigation; strict WARN/errors return 2. + +## Baseline and changed behavior + +Save the compliant report with its actual evidence: + +```bash +python -m shadecheck.cli baseline save --events out/my-client/events.jsonl --input out/my-client/report.json --output my-baseline.json +``` + +Capture your changed application to a different event file, then: + +```bash +python -m shadecheck.cli test --events out/changed/events.jsonl --rules SC-001 --config my-policy.json --baseline my-baseline.json --policy strict --output out/changed/report.json +``` + +Keep the backend, scenario and requirement comparable. The baseline validates existing evidence; it cannot exempt a known strict failure. A new request pattern does not by itself prove that a particular code change caused it. + +## Consumer GitHub Action + +Your preceding integration step must start/stop the observer and run the real client. Replace these example paths with the output of that step. The Action evaluates supplied evidence; it does not launch arbitrary wallet code automatically. + +This is a job fragment, not a complete workflow file: place `steps` under your own job, and set `permissions` at workflow or job scope. For a complete executable example, use the linked onboarding workflow. + +```yaml +permissions: + contents: read +steps: + - uses: actions/checkout@v4 + # Run the controlled backend, observer and real client here. + - name: Enforce the declared range policy and baseline + uses: 0xaje/ShadeCheck-@bbbbd166279edcbb92819dddaac6dc2443d6432e + with: + events: out/changed/events.jsonl + config: my-policy.json + baseline: my-baseline.json + rules: SC-001 + policy: strict + output-dir: out/privacy-gate + - uses: actions/upload-artifact@v4 + if: always() + with: + name: private-test-evidence + path: out/privacy-gate/ + retention-days: 7 +``` + +Configure this check as required in the consumer repository if you intend it to block merges. GitHub repository visibility and artifact permissions determine who can read uploaded evidence; the name “private-test-evidence” is not access control. Use throwaway regtest data for public CI, and review [evidence handling](evidence-privacy.md). + +The repository's [real onboarding workflow](../.github/workflows/developer-onboarding.yml) contains a complete executable consumer example with both positive and negative gates. Its negative case allows failure only temporarily, then requires exact exit 1 plus SC-001 and SC-005 findings. An arbitrary process error cannot make that workflow pass. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..e25d1a8 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,130 @@ +# Start using ShadeCheck + +ShadeCheck checks declared, observable Zcash application behavior and produces evidence-backed CI results. The public website displays recorded evidence. Fresh execution happens in your local checkout or GitHub Actions. + +Choose a path: + +| Goal | Requirements | What you get | +| --- | --- | --- | +| Inspect the current funded-wallet proof | Git, Python 3.11+; package dependencies | Recomputed saved evidence; no Docker, Rust, funds or wallet required | +| Run a small fresh developer workflow | Above plus Docker Linux engine and Compose v2 | Three real client traces, a baseline, HTML/JSON reports and tested CI decisions | +| Test the current independent wallet yourself | Docker, Rust and native build tools | A fresh funded Zingo / Zebra / Zaino test; [separate instructions](modern-wallet.md) | + +## 1. Install in a fresh user-owned directory + +Use your home directory, not Windows System32. Keep separate virtual environments for Windows and WSL/Linux. These commands clone a new checkout; they do not delete existing evidence or wallet data. + +Windows PowerShell: + +```powershell +cd $HOME +git clone https://github.com/0xaje/ShadeCheck- ShadeCheck +cd ShadeCheck +py -m venv .venv +.\.venv\Scripts\python.exe -m pip install . +``` + +Bash/WSL/Linux: + +```bash +cd ~ +git clone https://github.com/0xaje/ShadeCheck- ShadeCheck +cd ShadeCheck +python3 -m venv .venv +./.venv/bin/python -m pip install . +``` + +The first-run commands below use the environment's Python explicitly; activation is unnecessary. In the deeper integration guide, `python` means this same environment's interpreter. + +## 2. Inspect real recorded wallet evidence first + +PowerShell: + +```powershell +.\.venv\Scripts\python.exe -m zipfile -e examples/modern/recorded-regtest-20261024.zip out/modern-recorded +.\.venv\Scripts\python.exe scripts/modern_wallet.py --verify out/modern-recorded/20261024T135437-8bcfe43c +``` + +Bash/WSL: + +```bash +./.venv/bin/python -m zipfile -e examples/modern/recorded-regtest-20261024.zip out/modern-recorded +./.venv/bin/python scripts/modern_wallet.py --verify out/modern-recorded/20261024T135437-8bcfe43c +``` + +Expect `MODERN WALLET ARTIFACTS VERIFIED`. Open that directory's `report.html`. The two HIGH SC-002 findings describe observable broadcasts in explicit upstream local test transport. Verification success means saved evidence recomputed correctly; it does not mean the privacy report passed or that a fresh wallet ran on your computer. [Exact source CI and limits](../examples/modern/README.md). + +If you want to inspect the baseline-to-regression decision without Docker, the [original recorded developer walkthrough](../examples/range-client/README.md#original-recorded-onboarding-proof) has a separate downloadable archive and verifier. + +## 3. Run the fresh developer walkthrough + +Start Docker Desktop with Linux containers, or Docker Engine with Compose v2. Then: + +PowerShell: + +```powershell +.\.venv\Scripts\python.exe -m shadecheck.cli doctor +.\.venv\Scripts\python.exe scripts/developer_walkthrough.py +``` + +Bash/WSL: + +```bash +./.venv/bin/python -m shadecheck.cli doctor +./.venv/bin/python scripts/developer_walkthrough.py +``` + +`doctor` must report READY. This checks prerequisites only. The walkthrough starts the dedicated frozen regression backend, checks its regtest network, then runs a separate client process outside the checkout. The initial image build can take several minutes; keep the terminal open. It uses actual locally generated blocks and no public funds. No new transaction or wallet integration is claimed by this range-client example. + +The declared requirement is: **for this completed historical range, the block-serving observer must see a 20-block request beginning at a multiple of 20**. + +| Actual behavior | Privacy result | Strict CLI exit | Baseline comparison | +| --- | --- | --- | --- | +| Compliant request: 20 aligned blocks | PASS | 0 | Saved as baseline | +| Fresh unchanged request: same 20 blocks | PASS | 0 | PASS, zero regressions | +| Changed request: seven blocks starting three blocks later | FAIL, SC-001 | 1 | FAIL, one SC-005 regression | + +Every response is checked against the canonical node. The script exits 0 only after all expected outcomes have been verified, including the deliberately failing request. An execution error returns 2; it is never accepted as the expected policy failure. + +At completion, the terminal prints `REAL DEVELOPER WALKTHROUGH VERIFIED` and the exact output directory. Inspect: + +- `policy.json`: the requirement selected before the requests. +- `baseline-run/`, `unchanged-run/`, `changed-run/`: actual events, external-client receipts, node proofs and JSON/HTML reports. +- `baseline.json`: portable evidence-backed baseline. +- `changed-run-comparison.html`: the new regression and its current evidence. +- `walkthrough.json`: original artifact inventory and asserted exit codes. + +The program chooses a unique output directory by default. `--output PATH` requires a new directory and preserves existing files. It does not stop Docker after a local run; the backend remains available for integration work. Do not run overlapping suites against the same dedicated stack. + +To inspect a saved walkthrough later without starting Docker: + +```bash +python scripts/developer_walkthrough.py --verify out/developer-walkthrough/YOUR-RUN +``` + +This checks recorded consistency, not historical authorship or a new consensus execution. Replacing `YOUR-RUN` with the printed directory is required. + +## 4. Decide whether this policy belongs in your application + +This test is useful when your client deliberately promises fixed historical range batching to this observer. It does not prove wallet history, ownership or anonymity. Fetching 20 blocks rather than seven returns 13 additional blocks in this example; actual bandwidth and timing costs depend on their contents and transport. + +Live tip truncation, reorgs and resuming may legitimately require other patterns. Do not copy this strict policy onto every wallet. [Rule applicability](rule-applicability.md) explains exceptions. [Integrate a client](CLIENT_INTEGRATION.md) shows the observer and CI wiring. + +For your own implementation, fix the request scheduling that violates the intended requirement, then capture a fresh run. Do not repair evidence, loosen a requirement merely for a green badge, or replace a failing baseline without investigation. + +## If setup stops + +| Symptom | Next action | +| --- | --- | +| `doctor` is BLOCKED / Docker pipe missing | Start Docker Desktop, select Linux containers, and check `docker version` shows a Server. Rerun doctor. | +| Python virtual environment reports permission denied | Use a fresh checkout under your own home directory and a new user-owned environment. Do not install the project with sudo to bypass ownership. | +| Python cannot import grpc or shadecheck | Run pip install with the same environment's Python used to launch the script. | +| No completed range / backend is not ready | Wait for the initial backend startup to finish; inspect `python scripts/regtest.py check`. Do not substitute saved data for fresh execution. | +| Existing output directory | Use the default unique directory or choose a different `--output` path. | +| WARN or exit 2 | Inspect required coverage and the error. Missing evidence or execution failure is not a passing privacy test. | + +On Bash: `docker compose -f integration/compose.yml down` stops this dedicated backend while retaining its volumes. Do not add `-v` to routine shutdown. Windows users can run the same Docker command in PowerShell. + +## What this onboarding check establishes + +The automated workflow builds an actual wheel, installs it into a fresh virtual environment, runs the example client outside the checkout, validates real node-backed results, then exercises passing and failing consumer Actions. The [first real run](https://github.com/0xaje/ShadeCheck-/actions/runs/40103273814) completed successfully; its [original evidence](../examples/range-client/README.md#original-recorded-onboarding-proof) was downloaded and revalidated. Each future run must still pass CI and the artifact verifier. It does not establish that an unfamiliar person can follow the instructions unaided. Use the [independent reader exercise](ONBOARDING_REVIEW.md) to test that separately. diff --git a/docs/ONBOARDING_REVIEW.md b/docs/ONBOARDING_REVIEW.md new file mode 100644 index 0000000..9217017 --- /dev/null +++ b/docs/ONBOARDING_REVIEW.md @@ -0,0 +1,19 @@ +# Independent reader exercise + +Status: **not yet independently performed**. Automated fresh-environment CI is a separate check. Do not mark this exercise passed because the project's author or assistant can run the commands. + +Give an unfamiliar Zcash/Python developer only the repository URL and [getting-started guide](GETTING_STARTED.md). Ask them to use a fresh directory, throwaway local test data and their usual computer. Do not send seeds, private evidence or account credentials. + +Ask them to: + +1. Install ShadeCheck and explain whether they are inspecting saved evidence or running a fresh test. +2. Revalidate the saved current-wallet artifact and locate its findings and limitations. +3. Run doctor, resolve any setup block, and complete the fresh developer walkthrough. +4. Find the baseline, unchanged result and changed-request regression. +5. Explain why the harness can succeed while an intentionally violating privacy report fails. +6. Describe whether the example policy is appropriate for their own architecture and which code they would investigate. +7. Find how to connect a client and enforce a CI check. + +Record the actual OS/Python/Docker versions, source commit, time spent, help requested, exact failing commands, confusing instructions and whether each task completed. Keep personal details optional and private. A maintainer should fix observed problems, then have the reader retry only affected steps. + +There is no claimed completion time until it has been measured. If the reader needs live assistance, record it rather than calling the process unaided. Their successful exercise demonstrates one person's onboarding; it is not broad adoption, production compatibility or an anonymity audit. diff --git a/docs/RELEASE_VALIDATION.md b/docs/RELEASE_VALIDATION.md index a1196d5..4638eed 100644 --- a/docs/RELEASE_VALIDATION.md +++ b/docs/RELEASE_VALIDATION.md @@ -88,3 +88,11 @@ files/keys are included. Full scope and reproduction: [Zingo integration](zingo- The observer change also passed the existing [real MVP acceptance workflow](https://github.com/0xaje/ShadeCheck-/actions/runs/39597614943), including real backend scenarios and dashboard/browser checks. This new separate integration does not replace or merge the original 24-report acceptance snapshot. + +## Developer onboarding and useful CI decision — October 24 + +[Real onboarding run 40103273814](https://github.com/0xaje/ShadeCheck-/actions/runs/40103273814), source `a21f4ed8f46e70d704477e9e90467b11fcbcc0b1`, built an actual wheel, installed it into a fresh consumer virtual environment and ran a separate client process outside the checkout. The client exercised actual local block retrieval through ShadeCheck; all 47 returned block responses were checked against the canonical node. Fifty events were captured across three independent traces. + +A compliant baseline and fresh unchanged request both returned PASS/strict exit 0. The changed seven-block request returned SC-001 FAIL/exit 1; compatible baseline comparison returned one SC-005 regression. Both consumer Actions were executed, and the negative gate required exact exit 1 plus the expected SC-001 and SC-005 findings. Execution errors could not satisfy the negative gate. + +The original archive was downloaded, SHA-256 checked and revalidated offline; [evidence and provenance](../examples/range-client/README.md#original-recorded-onboarding-proof). All 59 local unit/boundary checks passed. New offline boundary tests replay the original real CI evidence and reject unknown manifest versions, unlisted nested files and altered receipts/comparisons even when their inventory hashes are updated. The example demonstrates a declared completed-range policy; it does not validate a full wallet algorithm or justify this policy for every architecture. Independent unfamiliar-reader testing remains explicitly unperformed; [exercise and recording instructions](ONBOARDING_REVIEW.md). diff --git a/docs/VIDEO_UPDATE_PROMPT.md b/docs/VIDEO_UPDATE_PROMPT.md index 58c9cbc..e814a04 100644 --- a/docs/VIDEO_UPDATE_PROMPT.md +++ b/docs/VIDEO_UPDATE_PROMPT.md @@ -32,3 +32,11 @@ Finish with: “Privacy CI for Zcash applications. Passing ShadeCheck does not e ## Delivery and submission Export MP4, confirm duration at or below two minutes, and watch the whole file for readability and truthful transitions. Upload the revised video if needed. If changing the hackathon video URL or description, press SUBMIT again after editing; the supplied form says saving a draft alone removes submitted status. Repository-only changes do not themselves change that form. Add the actual ShadeCheck video URL to README after it is supplied; never reuse another project's video. + +## Make the developer decision visible + +The new developer walkthrough in docs/GETTING_STARTED.md can replace the older generic local sync-comparison scene. Capture an actual completed local run, or clearly label its completed CI record as recorded evidence. Show the declared 20-block historical batching policy, the compliant baseline, a fresh unchanged request with zero regressions, and the changed seven-block request with SC-001 plus one SC-005 regression. Use the original generated reports; do not type statuses into an animation. + +Suggested short narration: “Our example client promises fixed historical block batches. An unchanged run passes. A narrower request violates that requirement, and the baseline comparison catches the regression. CI returns failure with inspectable evidence.” + +Keep a caption saying “Controlled protocol client · declared range policy”. This is separate from the funded Zingo test; do not claim the range-client change modified Zingo. Fetching additional blocks has a cost, and the example policy needs exceptions for tip, reorg and resume workflows. Preserve the two-minute total by shortening navigation and pauses. The independent reader exercise remains unperformed until a real unfamiliar person completes it. diff --git a/examples/range-client/README.md b/examples/range-client/README.md new file mode 100644 index 0000000..962a181 --- /dev/null +++ b/examples/range-client/README.md @@ -0,0 +1,27 @@ +# A real client integration example + +This separate Python process makes an actual GetBlockRange call to your controlled observer and requires delivery of every requested height. It uses ShadeCheck's installed protocol bindings and gRPC dependency. It is not a wallet, synthetic server, transaction generator or consensus verifier. + +Start with the [fresh developer walkthrough](../../docs/GETTING_STARTED.md), which installs/runs the package, captures three real traces, independently checks block hashes, saves a baseline and enforces the expected outcomes. The [client integration guide](../../docs/CLIENT_INTEGRATION.md) explains manual observer wiring and the consumer Action. + +`policy.json` is an example requirement for completed historical 20-block aligned ranges, not a universal wallet requirement or proof of anonymity. The [CI workflow](../../.github/workflows/developer-onboarding.yml) tests this example from a clean installed wheel; that is automated execution evidence, not independent user adoption. + +## Original recorded onboarding proof + +[Real CI run 40103273814](https://github.com/0xaje/ShadeCheck-/actions/runs/40103273814) passed at source `a21f4ed8f46e70d704477e9e90467b11fcbcc0b1`. It built a wheel, installed it into a fresh consumer environment, executed the external client outside the checkout, verified returned block hashes against the real node, and asserted both consumer Action outcomes. This is automated setup validation, not an independent person completing onboarding. + +The original `recorded-walkthrough-20261024.zip` is 56,714 bytes, SHA-256 `312991d6c82108a37af4135ea14439c253b111c8a14f521d641bd51fb5bfe4cf`. It was downloaded, hash checked and revalidated locally. It includes the walkthrough plus passing/failing consumer Action outputs, with no wallet files or keys. + +- Baseline and fresh unchanged requests: heights 80–99, PASS and strict exit 0. +- Changed request: heights 83–89, SC-001 FAIL and strict exit 1. +- Compatible baseline comparison: unchanged PASS with zero regressions; changed FAIL with one SC-005 regression. +- 50 actual observer events across three traces; 47 delivered block responses were checked against the canonical node. + +Inspect this saved workflow without Docker: + +```bash +python -m zipfile -e examples/range-client/recorded-walkthrough-20261024.zip out/onboarding-recorded +python scripts/developer_walkthrough.py --verify out/onboarding-recorded/onboarding-ci +``` + +Open `out/onboarding-recorded/onboarding-ci/changed-run-comparison.html` to see the developer decision. The verifier recomputes the walkthrough under `onboarding-ci`; the accompanying consumer Action files are separate CI outputs. Offline replay is not a fresh client execution or authenticated authorship. [Original provenance](provenance.json). diff --git a/examples/range-client/client.py b/examples/range-client/client.py new file mode 100644 index 0000000..541559f --- /dev/null +++ b/examples/range-client/client.py @@ -0,0 +1,35 @@ +"""Small real gRPC consumer example; not a wallet or synthetic RPC server.""" +import argparse +import json +import sys + +import grpc +from shadecheck.generated import service_pb2 as pb, service_pb2_grpc as rpc + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--server', required=True, help='Controlled ShadeCheck observer host:port') + parser.add_argument('--start', required=True, type=int) + parser.add_argument('--end', required=True, type=int) + args = parser.parse_args() + if args.start < 1 or args.end < args.start or args.end - args.start >= 1000: + parser.error('Choose an ascending non-genesis range of at most 1000 blocks') + with grpc.insecure_channel(args.server) as channel: + client = rpc.CompactTxStreamerStub(channel) + blocks = list(client.GetBlockRange(pb.BlockRange( + start=pb.BlockID(height=args.start), end=pb.BlockID(height=args.end)), timeout=30)) + if [b.height for b in blocks] != list(range(args.start, args.end + 1)): + raise RuntimeError('Backend did not deliver the complete requested range') + print(json.dumps({'requested_start': args.start, 'requested_end': args.end, + 'blocks': [{'height': b.height, 'block_hash': b.hash[::-1].hex()} for b in blocks]}, + sort_keys=True)) + return 0 + + +if __name__ == '__main__': + try: + raise SystemExit(main()) + except (RuntimeError, grpc.RpcError) as error: + print(f'Range client error: {error}', file=sys.stderr) + raise SystemExit(2) diff --git a/examples/range-client/policy.json b/examples/range-client/policy.json new file mode 100644 index 0000000..98610a6 --- /dev/null +++ b/examples/range-client/policy.json @@ -0,0 +1 @@ +{"schema_version":1,"sync":{"chunk_size":20,"alignment_height":0}} diff --git a/examples/range-client/provenance.json b/examples/range-client/provenance.json new file mode 100644 index 0000000..a421aeb --- /dev/null +++ b/examples/range-client/provenance.json @@ -0,0 +1,7 @@ +{ + "kind": "recorded-real-regtest-developer-walkthrough", + "source_run_url": "https://github.com/0xaje/ShadeCheck-/actions/runs/40103273814", + "shadecheck_revision": "a21f4ed8f46e70d704477e9e90467b11fcbcc0b1", + "artifact_id": 12563881217, + "archive_sha256": "312991d6c82108a37af4135ea14439c253b111c8a14f521d641bd51fb5bfe4cf" +} diff --git a/examples/range-client/recorded-walkthrough-20261024.zip b/examples/range-client/recorded-walkthrough-20261024.zip new file mode 100644 index 0000000..3e96ad2 Binary files /dev/null and b/examples/range-client/recorded-walkthrough-20261024.zip differ diff --git a/scripts/developer_walkthrough.py b/scripts/developer_walkthrough.py new file mode 100644 index 0000000..e5cede5 --- /dev/null +++ b/scripts/developer_walkthrough.py @@ -0,0 +1,187 @@ +"""Real external-client onboarding: baseline, unchanged run, changed request, CI gate. + +Uses only the dedicated local regression backend. Offline --verify starts nothing. +""" +import argparse +from contextlib import ExitStack +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import time +import uuid + +import grpc +import regtest as backend +from shadecheck.core import Recorder, evaluate, load_events +from shadecheck.doctor import inspect +from shadecheck.generated import service_pb2_grpc as rpc +from shadecheck.harness import start_server +from shadecheck.privacy import private_directory, private_write +from shadecheck.regression import compare_result, load_baseline + +ROOT = Path(__file__).resolve().parents[1] +POLICY = {'chunk_size': 20, 'alignment_height': 0} +SCENARIOS = {'baseline-run': 0, 'unchanged-run': 0, 'changed-run': 1} + + +def write(path, value): + private_write(path, json.dumps(value, indent=2, sort_keys=True) + '\n') + + +def cli(*args, expected=0): + result = subprocess.run([sys.executable, '-m', 'shadecheck.cli', *map(str, args)], + cwd=ROOT, capture_output=True, text=True, timeout=60) + if result.returncode != expected: + raise RuntimeError(f'CLI {args[0]} returned {result.returncode}; expected {expected}: {result.stderr.strip()}') + print(result.stdout.strip(), flush=True) + return result.returncode + + +def capture(root, name, start, end): + directory = root / name + private_directory(directory) + with ExitStack() as stack: + recorder = Recorder(directory / 'events.jsonl', 'upstream') + stack.callback(recorder.close) + channel = stack.enter_context(grpc.insecure_channel('127.0.0.1:9067')) + server, port = start_server(recorder, upstream=rpc.CompactTxStreamerStub(channel)) + stack.callback(lambda: server.stop(0).wait()) + # Different process, outside the checkout: imports must come from the + # installed package. A local in-process stub is not the consumer test. + process = subprocess.run([sys.executable, str(ROOT / 'examples/range-client/client.py'), + '--server', f'127.0.0.1:{port}', '--start', str(start), '--end', str(end)], + cwd=directory, capture_output=True, text=True, timeout=45) + if process.returncode: + raise RuntimeError(f'External range client failed (exit {process.returncode})') + receipt = json.loads(process.stdout) + write(directory / 'client-receipt.json', receipt) + proofs = [{'height': b['height'], 'block_hash': backend.node('getblockhash', b['height'])} + for b in receipt['blocks']] + if proofs != receipt['blocks']: + raise RuntimeError('External client received a noncanonical block') + write(directory / 'backend-proof.json', {'network': 'regtest', 'blocks': proofs}) + code = cli('test', '--events', directory / 'events.jsonl', '--rules', 'SC-001', + '--config', root / 'policy.json', '--policy', 'strict', + '--output', directory / 'report.json', expected=SCENARIOS[name]) + cli('report', '--input', directory / 'report.json', '--format', 'html', + '--output', directory / 'report.html') + return code + + +def verify(root): + root = root.resolve() + manifest = json.loads((root / 'walkthrough.json').read_text()) + if (manifest.get('schema_version') != 1 or manifest.get('kind') != 'shadecheck-developer-walkthrough' + or manifest.get('network') != 'regtest'): + raise ValueError('Not a controlled developer walkthrough') + inventory = manifest['artifacts'] + actual = {p.relative_to(root).as_posix() for p in root.rglob('*') if p.is_file() and p != root / 'walkthrough.json'} + if actual != {item['path'] for item in inventory} or len(actual) != len(inventory): + raise ValueError('Artifact inventory differs') + for item in inventory: + path = (root / item['path']).resolve() + if not path.is_relative_to(root) or hashlib.sha256(path.read_bytes()).hexdigest() != item['sha256']: + raise ValueError('Artifact path or hash mismatch') + if json.loads((root / 'policy.json').read_text()) != {'schema_version': 1, 'sync': POLICY}: + raise ValueError('Walkthrough policy changed') + results = {} + requests = {} + for name, expected in SCENARIOS.items(): + folder = root / name + events = load_events(folder / 'events.jsonl') + receipt = json.loads((folder / 'client-receipt.json').read_text()) + proof = json.loads((folder / 'backend-proof.json').read_text()) + start, end = receipt['requested_start'], receipt['requested_end'] + if [b['height'] for b in receipt['blocks']] != list(range(start, end + 1)): + raise ValueError('Incomplete external-client receipt') + if proof.get('network') != 'regtest' or proof['blocks'] != receipt['blocks']: + raise ValueError('Canonical node proof differs from client receipt') + requested = [e for e in events if e['method'] == 'GetBlockRange' and e['phase'] == 'request'] + delivered = [e for e in events if e['method'] == 'GetBlockRange' and e['phase'] == 'response'] + if (len(requested) != 1 or any(e['mode'] != 'upstream' for e in events) + or requested[0]['metadata']['start_height'] != start + or requested[0]['metadata']['end_height'] != end): + raise ValueError('Actual observer request differs from receipt') + observed = [{'height': e['metadata']['height'], + 'block_hash': bytes.fromhex(e['metadata']['block_hash'])[::-1].hex()} for e in delivered] + if observed != receipt['blocks']: + raise ValueError('Observer delivery differs from external-client receipt') + result = evaluate(events, rules=['SC-001'], sync_policy=POLICY) + if result != json.loads((folder / 'report.json').read_text()): + raise ValueError('Report differs from actual recorded evidence') + if result['status'] != ('FAIL' if expected else 'PASS') or manifest['strict_exits'][name] != expected: + raise ValueError('Unexpected policy status or recorded strict exit') + results[name] = result + requests[name] = (start, end) + start, end = requests['baseline-run'] + if end - start + 1 != 20 or start % 20 or requests['unchanged-run'] != (start, end) or requests['changed-run'] != (start + 3, start + 9): + raise ValueError('Walkthrough ranges do not represent the declared change') + baseline = load_baseline(root / 'baseline.json') + if baseline['result'] != results['baseline-run']: + raise ValueError('Baseline is not the captured compliant run') + for name, expected in [('unchanged-run', 0), ('changed-run', 1)]: + comparison = compare_result(results[name], baseline) + if comparison != json.loads((root / f'{name}-comparison.json').read_text()): + raise ValueError('Baseline comparison differs from recomputed evidence') + if comparison['baseline_comparison']['regressions'] != expected or manifest['comparison_exits'][name] != expected: + raise ValueError('Unexpected regression or comparison exit') + print(f'DEVELOPER WALKTHROUGH ARTIFACTS VERIFIED: {root}', flush=True) + return manifest + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, help='New output directory; existing evidence is never overwritten') + parser.add_argument('--verify', type=Path, help='Recompute a saved bundle without Docker or fresh RPCs') + args = parser.parse_args() + if args.verify: + if args.output: parser.error('Use --verify or --output, not both') + verify(args.verify) + return 0 + if inspect()['status'] != 'READY': + raise RuntimeError('Local prerequisites blocked. Run python -m shadecheck.cli doctor and fix its reported requirements.') + root = (args.output or ROOT / 'out/developer-walkthrough' / (time.strftime('%Y%m%dT%H%M%S') + '-' + uuid.uuid4().hex[:8])).resolve() + if root.exists(): raise ValueError('Choose a new output directory; existing evidence is preserved') + private_directory(root) + print('Starting dedicated local regression backend; first build can take several minutes...', flush=True) + backend.up() + available = min(backend.require_regtest()['blocks'], backend.lightd_info().blockHeight) + start = ((available + 1) // 20 - 1) * 20 + if start < 1: raise RuntimeError('No complete non-genesis historical chunk is available') + write(root / 'policy.json', {'schema_version': 1, 'sync': POLICY}) + exits = {} + for name, first, last in [('baseline-run', start, start + 19), ('unchanged-run', start, start + 19), ('changed-run', start + 3, start + 9)]: + print(f'External client: {name}, blocks {first}–{last}', flush=True) + exits[name] = capture(root, name, first, last) + cli('baseline', 'save', '--input', root / 'baseline-run/report.json', + '--events', root / 'baseline-run/events.jsonl', '--output', root / 'baseline.json') + comparisons = {} + for name, expected in [('unchanged-run', 0), ('changed-run', 1)]: + comparisons[name] = cli('compare', '--input', root / name / 'report.json', + '--events', root / name / 'events.jsonl', '--baseline', root / 'baseline.json', + '--output', root / f'{name}-comparison.json', expected=expected) + cli('report', '--input', root / f'{name}-comparison.json', '--format', 'html', + '--output', root / f'{name}-comparison.html') + write(root / 'walkthrough.json', { + 'schema_version': 1, 'kind': 'shadecheck-developer-walkthrough', 'network': 'regtest', + 'client': 'Separate Python range client using installed protocol bindings; not a full wallet', + 'strict_exits': exits, 'comparison_exits': comparisons, + 'artifacts': [{'path': p.relative_to(root).as_posix(), 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} + for p in sorted(root.rglob('*')) if p.is_file()], + 'limitations': ['Configured completed-range policy, not a universal privacy requirement or anonymity test.', + 'Live tip truncation, reorg/resume behavior and production privacy transports are not evaluated.', + 'Offline verification checks consistency; local node/driver are trusted and authorship is not authenticated.', + 'Automated clean-environment testing is not independent user adoption.']}) + verify(root) + print(f'REAL DEVELOPER WALKTHROUGH VERIFIED: unchanged PASS; changed request FAIL; new regression FAIL. Reports: {root}', flush=True) + return 0 + + +if __name__ == '__main__': + try: + raise SystemExit(main()) + except (OSError, RuntimeError, ValueError, KeyError, grpc.RpcError, subprocess.SubprocessError) as error: + print(f'Developer walkthrough error: {error}', file=sys.stderr) + raise SystemExit(2) diff --git a/tests/test_developer_walkthrough.py b/tests/test_developer_walkthrough.py new file mode 100644 index 0000000..69e2ed2 --- /dev/null +++ b/tests/test_developer_walkthrough.py @@ -0,0 +1,78 @@ +"""Replay original real CI evidence for offline integrity tests; no fresh RPC claim.""" +import hashlib +import importlib.util +import json +from pathlib import Path +import sys +import tempfile +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'scripts')) +try: + spec = importlib.util.spec_from_file_location('developer_walkthrough', ROOT / 'scripts/developer_walkthrough.py') + walkthrough = importlib.util.module_from_spec(spec) + spec.loader.exec_module(walkthrough) +finally: + sys.path.pop(0) + + +class WalkthroughEvidence(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + archive = ROOT / 'examples/range-client/recorded-walkthrough-20261024.zip' + self.assertEqual(hashlib.sha256(archive.read_bytes()).hexdigest(), + '312991d6c82108a37af4135ea14439c253b111c8a14f521d641bd51fb5bfe4cf') + with zipfile.ZipFile(archive) as z: + z.extractall(self.temp.name) + self.root = Path(self.temp.name) / 'onboarding-ci' + + def rehash(self, *names): + path = self.root / 'walkthrough.json' + manifest = json.loads(path.read_text()) + for item in manifest['artifacts']: + if item['path'] in names: + item['sha256'] = hashlib.sha256((self.root / item['path']).read_bytes()).hexdigest() + path.write_text(json.dumps(manifest)) + + def test_original_recorded_run_recomputes_without_backend(self): + result = walkthrough.verify(self.root) + self.assertEqual(result['strict_exits'], {'baseline-run': 0, 'unchanged-run': 0, 'changed-run': 1}) + self.assertEqual(result['comparison_exits'], {'unchanged-run': 0, 'changed-run': 1}) + + def test_extra_nested_manifest_cannot_escape_inventory(self): + folder = self.root / 'extra' + folder.mkdir() + (folder / 'walkthrough.json').write_text('{}') + with self.assertRaisesRegex(ValueError, 'inventory differs'): + walkthrough.verify(self.root) + + def test_unknown_manifest_version_is_rejected(self): + path = self.root / 'walkthrough.json' + value = json.loads(path.read_text()); value['schema_version'] = 99 + path.write_text(json.dumps(value)) + with self.assertRaisesRegex(ValueError, 'controlled developer walkthrough'): + walkthrough.verify(self.root) + + def test_edited_receipt_and_node_proof_still_must_match_recorded_delivery(self): + names = ['changed-run/client-receipt.json', 'changed-run/backend-proof.json'] + for name in names: + path = self.root / name + value = json.loads(path.read_text()) + value['blocks'][0]['block_hash'] = '00' * 32 + path.write_text(json.dumps(value)) + self.rehash(*names) + with self.assertRaisesRegex(ValueError, 'Observer delivery differs'): + walkthrough.verify(self.root) + + def test_rehashed_comparison_cannot_hide_a_regression(self): + name = 'changed-run-comparison.json' + path = self.root / name + value = json.loads(path.read_text()) + value['baseline_comparison']['regressions'] = 0 + path.write_text(json.dumps(value)) + self.rehash(name) + with self.assertRaisesRegex(ValueError, 'comparison differs'): + walkthrough.verify(self.root)