diff --git a/.github/workflows/labs.yml b/.github/workflows/labs.yml new file mode 100644 index 0000000..969391e --- /dev/null +++ b/.github/workflows/labs.yml @@ -0,0 +1,49 @@ +name: Labs + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + labs: + runs-on: ubuntu-22.04 # still has Python 3.8 builds + strategy: + matrix: + python: ["3.8", "3.12"] # labs promise Python 3.8+ + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Silent Payments reference solutions pass all BIP352 vectors + working-directory: privacy-track/labs/silent-payments + run: | + python3 run_vectors.py send --solution + python3 run_vectors.py receive --solution + + - name: Unimplemented exercises are skipped, not crashed + working-directory: privacy-track/labs/silent-payments + run: | + for side in send receive; do + out=$(python3 run_vectors.py $side || true) + echo "$out" | tail -3 + if echo "$out" | grep -q "crashed"; then echo "::error::$side stub crashed"; exit 1; fi + done + python3 scan_benchmark.py 20 + + - name: Other labs run end to end + run: | + for lab in chain-analysis/chain_analysis_lab.py coin-selection/coin_selection_simulator.py compact-block-filters/compact_block_filters.py; do + (cd privacy-track/labs/$(dirname $lab) && python3 $(basename $lab) > /dev/null) + done + + links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: python3 tools/check_links.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 9ba9973..b823434 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ All materials are released under **CC0 1.0 Universal (Public Domain)** - use the ### Recommended Learning Path ``` -Bitcoin Dojo (7 weeks) ──> rawBit (10 weeks) ──> Decoding Bitcoin (8 weeks) - │ │ - └── Sovereign Bitcoiner (5-10 weeks) └── Open-source contribution +Bitcoin Dojo (7 weeks) ──> rawBit (10 weeks) ──> Decoding Bitcoin (8 weeks) ──> Open-source contribution + │ ▲ + └── Sovereign Bitcoiner (5-10 weeks) Privacy Track (drop-in, biweekly) ─┘ ``` | Program | Duration | Level | Directory | @@ -30,6 +30,7 @@ Bitcoin Dojo (7 weeks) ──> rawBit (10 weeks) ──> Decoding Bitcoin (8 wee | **rawBit Study Cohort** | 10 weeks | Intermediate | [`/rawbit/`](./rawbit/) | | **Decoding Bitcoin** | 8 weeks | Intermediate-Advanced | [`/decoding-bitcoin/`](./decoding-bitcoin/) | | **Sovereign Bitcoiner Crash Course** | 5-10 weeks | Beginner-Intermediate | [`/sovereign-bitcoiner/`](./sovereign-bitcoiner/) | +| **Privacy Track** | 12 biweekly sessions, drop-in | Intermediate | [`/privacy-track/`](./privacy-track/) | | **Vibe Coding on Nostr** | Monthly workshops | All levels | [`/nostr-workshops/`](./nostr-workshops/) | | **Workshop Modules** | Single session | Varies | [`/workshops/`](./workshops/) | @@ -96,6 +97,9 @@ curriculum/ │ ├── week-06-psbt/ │ ├── week-07-bitcoin-core/ │ └── week-08-contributing/ +├── privacy-track/ # The Privacy Sessions: 12 drop-in sessions + labs +│ ├── README.md, SESSIONS.md # Overview and session plans (starts with Silent Payments) +│ └── labs/ # Offline Python labs (BIP352 vectors, chain analysis, ...) ├── sovereign-bitcoiner/ # 5-10 week crash course │ ├── README.md │ ├── week-01-philosophy/ diff --git a/privacy-track/CONTRIBUTING.md b/privacy-track/CONTRIBUTING.md index 73874d3..74a96a4 100644 --- a/privacy-track/CONTRIBUTING.md +++ b/privacy-track/CONTRIBUTING.md @@ -14,13 +14,13 @@ If you find a technical error (wrong BIP reference, incorrect code, broken link) The exercises are designed to be challenging but achievable. If you completed one and found it too easy, too hard, or unclear, submit a PR with improvements. Include a note explaining what you changed and why. -### Add a new exercise +### Add a new lab -New exercises are welcome, especially for: -- Module 1 (chain analysis): real-world transaction analysis using mainnet data -- Module 3 (Payjoin): exercises using the Payjoin Dev Kit in Rust -- Module 4 (wallet privacy): CoinJoin simulation, timing analysis exercises -- Any module: exercises in Rust (current exercises are Python-heavy) +New labs are welcome, especially: +- A wallet-fingerprinting lab built on **real** transaction hex from the S4 fingerprint table (not invented wallet behavior) +- A payjoin lab that runs against `payjoin-cli` on regtest +- Rust versions of the Python labs +- Season Two topics (see [SESSIONS.md](./SESSIONS.md)) ### Translate the curriculum @@ -28,7 +28,7 @@ If you want to translate this curriculum into another language, create a new dir ### Add reference implementations -If you've completed the exercises and want to share your solutions as reference implementations, add them to `exercises/solutions/` in the relevant module. Name them clearly (e.g., `chain_analysis_lab_solution.py`). +Solutions live next to their lab in `labs//solutions/`. If the lab has a grader (like `labs/silent-payments/run_vectors.py`), your solution must pass it. --- @@ -47,20 +47,28 @@ If you've completed the exercises and want to share your solutions as reference - Python exercises: Python 3.8+, minimal dependencies, clear docstrings, type hints - Rust exercises: stable Rust, idiomatic style, well-commented - All exercises must include: clear instructions, type signatures, test cases, and reflection questions -- Never include complete solutions in the exercise files - only hints, pseudocode, and test cases +- Never include complete solutions in the exercise files: only hints, pseudocode and tests. Solutions go in `solutions/` ### File structure ``` -module-XX-topic/ -├── README.md # Session-by-session breakdown -├── exercises/ -│ ├── exercise_name.py # Coding exercise -│ ├── exercise_name.md # Non-coding exercise (analysis, discussion) -│ └── solutions/ # Reference implementations (optional) -└── resources/ # Additional reading, diagrams, data files +privacy-track/ +├── README.md, SESSIONS.md # overview and the 12 session plans +├── FACILITATOR_GUIDE.md # how to run any session +├── ISSUE_POOL.md, PR_CHECKLIST.md +├── labs// +│ ├── README.md # what you build, how to run it +│ ├── *.py # exercise files with YOUR CODE HERE markers +│ └── solutions/ # reference implementations (optional) +└── resources/ # glossary, reading list ``` +### Accuracy rules + +- Every factual claim about a wallet, BIP or PR should be checkable. Link the source +- Prefer official test vectors over hand-made test data. When a lab simplifies the spec, say so in its README +- Exercise files must run without errors when nothing is implemented yet (skip unimplemented parts gracefully) + --- ## Review Process diff --git a/privacy-track/FACILITATOR_GUIDE.md b/privacy-track/FACILITATOR_GUIDE.md index 9a10752..7d82727 100644 --- a/privacy-track/FACILITATOR_GUIDE.md +++ b/privacy-track/FACILITATOR_GUIDE.md @@ -1,350 +1,88 @@ -# Facilitator Guide - Bitcoin Privacy Developer Track +# Facilitator Guide: The Privacy Sessions -> Everything you need to run this track at your own Bitcoin dev school, hackerspace, or study group. +How to host one 90-minute session well. The per-session content (story, concept, hands-on, PRs, follow-ups) lives in [SESSIONS.md](./SESSIONS.md). This page covers everything that stays the same from session to session. --- -## Before You Start +## Your role -### Who This Track Is For +You're a host, not a professor. You need to be one session ahead of the room, have done the hands-on yourself, and be comfortable saying "I don't know, let's read the code." Two facilitators per session is ideal: one drives the screen, the other watches chat and unblocks people. -Participants should already understand how Bitcoin transactions work at the technical level. Specifically, they should be able to explain what a UTXO is, read a simple Bitcoin Script, and understand how digital signatures authorize spending. If your participants haven't completed a foundational Bitcoin development course (like Bitcoin Dojo, Decoding Bitcoin, or Chaincode seminars), they will struggle with the material starting in Module 2. +## The week before -The track is designed for groups of 5-15 participants. Smaller groups work better for the hands-on sessions; larger groups work fine for the lecture-style sessions if you break into smaller teams for exercises. +- [ ] Do the session's hands-on end to end on a clean machine. Write down every gotcha +- [ ] Pick 1-2 live PRs for the Review Circle. Good picks are small, privacy-relevant, active in the last month, and have a clear "how would you test this?" angle. For S3 (Review Club), announce the PR a week ahead and ask people to build it +- [ ] Pull 3-5 targets from [ISSUE_POOL.md](./ISSUE_POOL.md) for the Proof-of-Work Board +- [ ] Post the session announcement with: the topic, what to install, the lab link, and "drop-ins welcome" -### What You Need +## The morning of -**Technical requirements:** -- Each participant needs a laptop with Python 3.8+ and a text editor -- Bitcoin Core installed (for regtest exercises in Modules 3-4) -- Rust toolchain installed (for Module 3 Payjoin Dev Kit and Module 4 contributions) -- Internet access for reading BIPs and browsing repos during sessions +- [ ] Re-check that every issue on the board is still open and unclaimed +- [ ] Re-check that the Review Circle PR hasn't merged or changed direction (if it merged, that's fine: review the merged diff and read the discussion that got it there) +- [ ] Fund the signet/regtest wallets you'll use on screen +- [ ] Have the primary text for the Cypherpunk Corner open -**Environment setup (do this BEFORE the first session):** +## Running the room -```bash -# Create a shared regtest environment -bitcoind -regtest -daemon -bitcoin-cli -regtest createwallet "facilitator" -ADDR=$(bitcoin-cli -regtest -rpcwallet=facilitator getnewaddress) -bitcoin-cli -regtest generatetoaddress 200 $ADDR +| Beat | Your job | +|---|---| +| 🩸 Cold Open | Tell the story and stop. Let the room react before you explain anything | +| 🕳 Rabbit Hole | One analogy, one diagram, then straight to code. 15 min max | +| 🛠 Hands-On | Get everyone to a first success in 10 minutes. Pair stuck people with finished ones. Don't live-debug one person's setup in front of everyone; take it to a breakout | +| 🔍 Review Circle | Participants explain, you ask questions. See the rules below | +| 📜 Cypherpunk Corner | Pick a reader and let the argument happen. Steelman the side nobody takes | +| 🎯 Proof-of-Work Board | Everyone names one concrete follow-up out loud. Write the names down | -# Verify Python exercises run -cd module-02-silent-payments/exercises/ -python3 -c "from silent_payments_sender import *; print('Setup OK')" +## Review Circle rules -# Verify Rust toolchain -rustc --version # Should be 1.70+ -cargo --version -``` +These come from the program's [Review Club playbook](https://github.com/code-orange-dev/code-orange-dev/blob/main/privacy-contributor-program/REVIEW_CLUB.md), condensed for a 20-minute slot: -**Room setup:** -- Projector or large screen for live coding -- Whiteboard for diagrams (elliptic curves, transaction flows, ECDH) -- Printed copies of the BIP352 and BIP77 specifications (optional but useful) +1. **Participants explain, you ask.** "What problem does this solve?" "Where's the test?" "What would break if...?" +2. **Test before you opine.** A build-and-run report beats a guess +3. **Nothing goes upstream on a timer.** Post only what's genuinely useful: a reproduction, a test report with exact steps and platform, or a question the thread hasn't already answered. If nothing qualifies, say so. That's a good outcome too +4. **Never paste your words into someone else's mouth.** Don't dictate comments for participants to post +5. **Separate questions from objections**, and respect each project's norms (read its CONTRIBUTING first) -### Pacing +## Evidence and rewards -The track is designed for two pacing options: +- After each session, log for each participant who produced something: what was built/tested/reviewed, links, and the next step. The format is in the program's [CONTRIBUTOR_SCORECARD.md](https://github.com/code-orange-dev/code-orange-dev/blob/main/privacy-contributor-program/CONTRIBUTOR_SCORECARD.md) +- Sats go to **completed proof of work**: a lab at 28/28, a test report someone else can follow, a merged doc fix, a reproduced bug. Never pay per comment or per PR opened. That's how programs end up spamming maintainers +- Merged PRs go on the [PR dashboard](https://github.com/code-orange-dev/PR-tracking-dashboard) +- Within 48 hours, send attendees: what we covered (2 lines), their one follow-up, and the next session date ([community playbook](https://github.com/code-orange-dev/community-playbook)) -| Pace | Duration | Sessions/week | Best for | -|------|----------|---------------|----------| -| Intensive | 6 weeks | 2x per week, 2-3 hours each | Full-time cohorts, bootcamps | -| Standard | 12 weeks | 1x per week, 2-3 hours each | Working developers, study groups | +## Common problems -Each session has more material than can be covered in one sitting. This is intentional - it's better to have too much than too little. Prioritize the exercises over the lecture content. Participants learn more by doing than by listening. +**"The EC math is too hard."** The SP lab's `secp.py` hides the math behind `a * G` and `P + Q`, so participants only need to know that points add and scalars multiply. For the curious, point them to the [Bitcoin Dojo](../bitcoin-dojo/) weeks 1-2. Draw it on a whiteboard with small numbers first. ---- - -## Session-by-Session Plan - -### Module 1: Chain Analysis & Why Privacy Matters - -#### Session 1: How Bitcoin Surveillance Works (2.5 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:20 | **Intro and context** | Why this track exists. Show the OpenSats blog post on privacy. Ask: "How many of you have reused a Bitcoin address?" | -| 0:20-0:50 | **Lecture: The four heuristics** | CIOH, change detection, address reuse clustering, timing analysis. Use whiteboard to draw example transactions. | -| 0:50-1:00 | **Break** | | -| 1:00-1:45 | **Hands-on: Chain Analysis Lab** | `exercises/chain_analysis_lab.py` - Participants work through Exercises 1-4. Walk around and help. | -| 1:45-2:15 | **Live demo: OXT.me** | Pick a real mainnet transaction and trace it live. Show how CIOH + change detection reveals the entity. | -| 2:15-2:30 | **Discussion** | Use the discussion questions from the README. Key insight: these heuristics are assumptions, not facts. Payjoin and CoinJoin exploit this. | - -**Facilitator tips:** -- The chain analysis lab has 8 sample transactions designed to illustrate different patterns. TX 3 is a Payjoin (CIOH is wrong). TX 6 is a CoinJoin. Make sure participants discover this. -- If participants finish early, have them attempt Exercise 6 (entity tracing across multiple transactions). -- The reflection questions at the end are important - assign them as homework if you run out of time. - -#### Session 2: Privacy as a Protocol Property (2 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:15 | **Review homework** | Ask 2-3 participants to share their chain analysis lab results. What surprised them? | -| 0:15-0:45 | **Lecture: Greg Maxwell's 2013 post** | Read key excerpts aloud. The "birth control" example. Why privacy matters for fungibility. | -| 0:45-1:15 | **Group exercise: Privacy spectrum** | Draw a spectrum on the whiteboard: Transparent → Pseudonymous → Private → Anonymous. Have groups place different Bitcoin usage patterns on the spectrum. | -| 1:15-1:30 | **Break** | | -| 1:30-1:50 | **Lecture: Privacy improvements that help everyone** | Silent Payments, Payjoin, Taproot. The anonymity set concept. | -| 1:50-2:00 | **Assign essay** | 500-word essay: "What is the most impactful privacy improvement that could be made to Bitcoin today, and why?" Due before Session 3. | - -**Facilitator tips:** -- This session is more discussion-heavy than technical. That's intentional - participants need to internalize WHY privacy matters before diving into HOW. -- If the group is technical and engaged, introduce the concept of "privacy by default vs privacy by choice" and ask them to debate which is better. -- The essay is a good filter: participants who write thoughtful essays will get the most out of the technical sessions. - ---- - -### Module 2: Silent Payments - BIP352 - -#### Session 3: How Silent Payments Work (2.5 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:15 | **Essay discussion** | Ask 2-3 volunteers to summarize their essays. Note common themes. | -| 0:15-0:30 | **Lecture: The address reuse problem** | Show a real donation address (e.g., a public Bitcoin charity). Show all incoming payments on mempool.space. "This is why we need Silent Payments." | -| 0:30-1:00 | **Lecture: ECDH and key derivation** | Use the whiteboard to draw the ECDH key exchange. Emphasize: both parties derive the same shared secret without communicating. This is the magic. | -| 1:00-1:10 | **Break** | | -| 1:10-1:30 | **Lecture: BIP352 walkthrough** | Walk through the specification step by step. Have participants follow along with the PDF/web page open. | -| 1:30-2:10 | **Whiteboard exercise: manual Silent Payment** | Work through a complete Silent Payment by hand on the whiteboard. Pick simple numbers. Compute the shared secret, the tweak, and the output key. | -| 2:10-2:30 | **Preview next session** | Show the `silent_payments_sender.py` exercise. Explain what they'll implement. Assign: read BIP352 Sections 1-3 before the next session. | - -**Facilitator tips:** -- The ECDH explanation is the hardest part. Use small numbers first (e.g., G=5, private key=3, public key=15). Then relate to secp256k1. -- If the group has already done the Bitcoin Dojo ECC exercises, reference those directly. -- The whiteboard exercise is critical. Don't skip it. If participants can compute a Silent Payment by hand, the code will make sense. - -#### Session 4: Implement a Minimal SP Sender (3 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:15 | **Review: BIP352 key concepts** | Quick whiteboard recap of ECDH, scan/spend keys, shared secret derivation | -| 0:15-1:15 | **Coding: Exercises 1-3** | `silent_payments_sender.py` - Generate keypair, derive shared secret, compute output key. Walk around and help. | -| 1:15-1:25 | **Break** | | -| 1:25-2:15 | **Coding: Exercise 4** | Full Silent Payment simulation. Run tests. Debug together. | -| 2:15-2:45 | **Code review** | Have 1-2 participants share their screen and walk through their implementation. Discuss edge cases. | -| 2:45-3:00 | **Discussion** | Why sum all input keys? What about Taproot inputs? What happens with incorrect parity? | - -**Facilitator tips:** -- The pure-Python ECC operations are slow. That's fine - it's for learning, not production. -- Common errors: forgetting `% N` for modular arithmetic, incorrect byte ordering for tagged hashes, confusing x-coordinate-only vs full point serialization. -- If participants finish early, challenge them to implement label support (BIP352 Section 5). - -#### Session 5: Scanning and Receiving (3 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:20 | **Lecture: The scanning problem** | "You've built the sender. Now the hard part: how does the receiver find payments?" Calculate mainnet numbers: 300K tx/day × 1 ECDH each. | -| 0:20-1:00 | **Coding: scanner exercises** | `silent_payments_scanner.py` - scan_transaction(), scan_block() | -| 1:00-1:10 | **Break** | | -| 1:10-1:50 | **Coding: benchmark** | Exercise 3 - benchmark scanning performance. Measure tx/s and extrapolate to mainnet. | -| 1:50-2:30 | **Lecture + Discussion: Making it practical** | Compact block filters (preview Module 4), light client trade-offs, Kyoto project | -| 2:30-3:00 | **Planning next session** | "Next session: you'll contribute to a real Silent Payments implementation." Browse open issues together. Each participant picks a target repo. | - -**Facilitator tips:** -- The benchmark exercise is eye-opening. Participants will see that scanning mainnet takes minutes (not seconds) on a laptop, and much longer on a phone. -- This naturally motivates Module 4 (compact block filters) - plant that seed now. -- Spend the last 30 minutes browsing GitHub issues together. This makes Session 6 much more productive. - -#### Session 6: Contributing to Silent Payments (2.5 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:30 | **Code walkthrough: Bitcoin Core #28122** | Screen-share the PR. Walk through the key files. Show the test vectors. | -| 0:30-1:00 | **Code walkthrough: rust-silentpayments** | API tour, example usage, open issues | -| 1:00-1:10 | **Break** | | -| 1:10-2:00 | **Work time** | Participants fork their chosen repo, set up the dev environment, start working on their chosen issue. Help individuals. | -| 2:00-2:30 | **Check-in and next steps** | Each participant shares what they're working on and any blockers. Set a deadline for the PR (typically 1-2 weeks). | - -**Facilitator tips:** -- Not everyone will submit a PR during the session. That's fine. The goal is to get them past the "I don't know where to start" barrier. -- Good first contributions: documentation improvements, test coverage, fixing typos in specifications, adding examples. -- If Bitcoin Core feels too intimidating, steer participants toward rust-silentpayments or wallet integrations. - ---- - -### Module 3: Payjoin - BIP77/78 - -#### Session 7: How Payjoin Breaks Chain Analysis (2.5 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:20 | **Lecture: CIOH is the backbone of surveillance** | Recap from Module 1. "We're going to break it." | -| 0:20-0:50 | **Lecture: Payjoin V1 → V2** | The liveness problem. How V2 solves it with directories. | -| 0:50-1:00 | **Break** | | -| 1:00-1:30 | **Whiteboard exercise: Payjoin step by step** | Walk through the 9-step Payjoin flow on the whiteboard. Both participants play sender and receiver. | -| 1:30-2:00 | **Exercise: Ambiguity analysis** | Use the exercises from the README. Given a transaction, how many interpretations exist? | -| 2:00-2:30 | **Discussion** | "If every merchant used Payjoin, what would happen to chain analysis as an industry?" | - -#### Session 8: Building with Payjoin Dev Kit (3 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:20 | **PDK API overview** | Screen-share the rust-payjoin docs | -| 0:20-1:30 | **Hands-on: regtest Payjoin** | `exercises/payjoin_testnet.md` - Exercises 1-2 | -| 1:30-1:40 | **Break** | | -| 1:40-2:30 | **Hands-on: compare normal vs Payjoin** | Exercise 2 - side-by-side comparison | -| 2:30-3:00 | **Discussion questions** | Coin selection for receivers, sender verification checks, mobile UX | - -#### Session 9: Contributing to Payjoin (2.5 hours) - -Same structure as Session 6. Code walkthrough of payjoin-rust and BTCPay Server, then guided contribution time. - ---- - -### Module 4: Privacy-Preserving Wallet Development - -#### Session 10: Transaction Construction for Privacy (3 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:30 | **Lecture: The invisible fingerprints** | nLockTime, nSequence, version, fee estimation, output ordering | -| 0:30-1:30 | **Coding: Coin Selection Simulator** | `exercises/coin_selection_simulator.py` - Implement 4 algorithms | -| 1:30-1:40 | **Break** | | -| 1:40-2:30 | **Coding: Comparison analysis** | Run all algorithms against different UTXO sets, analyze privacy scores | -| 2:30-3:00 | **Discussion** | "If you were building a wallet from scratch for maximum privacy, what defaults would you choose?" | - -#### Session 11: Compact Block Filters (3 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:20 | **Lecture: Why SPV is broken for privacy** | BIP37 Bloom filters leak your addresses to the server | -| 0:20-0:40 | **Lecture: Golomb-Rice coding** | Use whiteboard to work through encoding by hand | -| 0:40-1:30 | **Coding: Build a filter** | `exercises/compact_block_filters.py` - Exercises 1-3 | -| 1:30-1:40 | **Break** | | -| 1:40-2:20 | **Coding: Filter analysis** | Exercise 4 - measure size, FP rate, query time | -| 2:20-2:50 | **Coding: SP + CBF simulation** | Exercise 5 - how filters make Silent Payments viable on mobile | -| 2:50-3:00 | **Preview capstone** | Introduce the capstone project (Module 4 Session 3) | - -#### Session 12: CoinJoin, CoinSwap, and the Future (2.5 hours) - -| Time | Activity | Notes | -|------|----------|-------| -| 0:00-0:30 | **Lecture: CoinJoin and WabiSabi** | Equal-output mixing, anonymity sets, coordinator trust | -| 0:30-0:50 | **Lecture: CoinSwap and Teleport** | Atomic swaps for privacy, multi-hop routing | -| 0:50-1:00 | **Break** | | -| 1:00-1:20 | **Lecture: The future** | CISA, Ark, Taproot-enabled privacy | -| 1:20-2:00 | **Capstone project introduction** | Explain requirements, review rubric, help participants choose topics | -| 2:00-2:30 | **Final discussion** | "What privacy improvement would you work on if you had a year of funding?" Reflect on the track as a whole. | - ---- - -## Assessment Framework - -### Grading Philosophy - -This track produces Bitcoin open-source contributors, not exam-takers. Assessment is based on demonstrated ability to understand, implement, and contribute. - -### Components - -| Component | Weight | Description | -|-----------|--------|-------------| -| Exercises | 30% | Completion of all Python/Rust exercises across modules | -| Session 2 Essay | 10% | 500-word essay on privacy improvements | -| PR Submissions | 30% | At least 1 PR to a Silent Payments or Payjoin repo | -| Capstone Project | 30% | Final report + implementation (see capstone/README.md) | - -### Exercise Rubric - -| Score | Criteria | -|-------|----------| -| Complete (100%) | All functions implemented, tests pass, reflection questions answered thoughtfully | -| Partial (70%) | Core functions implemented, some tests pass, reflection questions attempted | -| Started (40%) | Some functions implemented, demonstrates understanding of the concepts | -| Not submitted (0%) | No submission | - -### PR Rubric - -| Score | Criteria | -|-------|----------| -| Merged (100%) | PR accepted and merged by maintainers | -| Under review (80%) | PR submitted, tests pass, responding to review comments | -| Submitted (60%) | PR submitted with clear description | -| Draft (40%) | Draft PR or work-in-progress with progress toward completion | -| Not submitted (0%) | No PR attempted | - ---- - -## Guest Speaker Suggestions - -These developers are actively working on the privacy projects covered in this track. Inviting one or two as guest speakers adds enormous credibility and motivation. - -| Name | Project | Topic | -|------|---------|-------| -| Josie Baker | Bitcoin Core Silent Payments | BIP352 design decisions, scanning optimization | -| Dan Gould | Payjoin Dev Kit | PDK architecture, BIP77 implementation | -| Ruben Somsen | BIP352 co-author | Silent Payments theory, future improvements | -| Murch | Bitcoin Core coin selection | Branch-and-bound, waste metric, privacy trade-offs | -| Chris Belcher | CoinSwap/Teleport | CoinSwap design, JoinMarket NG | -| Rob (rustaceanrob) | Kyoto light client | BIP157/158 implementation, BDK integration | - -**How to invite:** -- Reach out via GitHub, Twitter/X, or Nostr -- Offer a 30-45 minute slot (video call is fine) -- Share the curriculum in advance so they know the audience level -- Ask them to focus on "what needs to be built" - your participants are looking for contribution opportunities - ---- - -## Common Problems and Solutions - -**"The ECC math is too hard"** -- Point them back to the Bitcoin Dojo Week 1-2 exercises (FieldElement, Point, etc.) -- Use small numbers on the whiteboard before jumping to secp256k1 -- Pair struggling participants with stronger ones - -**"I can't find an issue to work on"** -- Documentation improvements always exist -- Test coverage gaps are easy to identify with `cargo tidy` or `gcov` -- Translating error messages is valuable and approachable -- Reviewing other people's PRs counts as contribution - -**"Bitcoin Core is too intimidating"** -- Start with rust-silentpayments (smaller codebase, friendlier maintainers) -- BDK and Kyoto are also good entry points -- BTCPay Server's Payjoin code is in C# and well-documented - -**"I don't have a full node"** -- All exercises work on regtest (no download required) -- Signet is lightweight enough for laptop use -- For the chain analysis lab, mempool.space provides the data - -**"We only have 1 hour per session"** -- Cut the lecture portions to 10-15 minutes -- Assign reading as pre-work -- Focus on one exercise per session instead of the full set -- Extend the track to 18-24 weeks - ---- +**"My build failed."** Keep a pre-built binary or a shared regtest box around. Setup pain belongs in a breakout room, and afterwards it makes a great docs PR. -## After the Track +**"I can't find anything to contribute."** A test report on an open PR is a contribution. So are docs. So is a reproduction of an open bug. The [pool](./ISSUE_POOL.md) is curated for exactly this. -### Contribution Sprint +**"Bitcoin Core is intimidating."** Start with the SP lab, which uses the exact vectors in `src/test/data/bip352_send_and_receive_vectors.json`. Then read `src/common/bip352.cpp`, which is short. That's a much gentler entry to Core than most. -The track naturally leads into a contribution sprint. Participants should: +**"We only have 60 minutes."** Drop the Cypherpunk Corner, assign the Rabbit Hole reading as pre-work, and keep Hands-On + Review Circle. -1. Continue working on their PRs from Sessions 6 and 9 -2. Pick a new issue from the target repos and start a second PR -3. Review each other's PRs (cross-review builds community) -4. Present their capstone projects to the group +**"Nobody came back."** Check that the 48-hour follow-up went out, that the hands-on actually worked for newcomers, and that the session time suits your timezones. -### Staying Connected +## Guest speakers -- Add graduates to a dedicated Discord/Telegram channel for ongoing support -- Share relevant Bitcoin Optech newsletters and PR updates -- Organize monthly "privacy office hours" where graduates can get help with contributions -- Track merged PRs - this is the metric that matters for grant renewals +Builders working on this season's topics. Invite one per month. Offer 30 minutes, share the session plan, and ask them to talk about *what still needs building and testing*. -### Measuring Success +| Topic | People to ask (verify they're still active) | +|---|---| +| Silent Payments (BIP352) | Josie Baker and Ruben Somsen (BIP authors), Eunovo (Bitcoin Core SP PRs), cygnet3 (rust-silentpayments, Dana), setavenger (BlindBit), nymius (bdk-sp), Craig Raw (Sparrow, Frigate) | +| Payjoin | Dan Gould and the Payjoin Dev Kit team | +| Coin selection | Murch | +| Light clients | Rob Netzke / rustaceanrob (Kyoto), the Floresta maintainers (Vinteum) | +| Network privacy | Vasil Dimov (private broadcast), 0xB10C (peer-observer) | +| CoinSwap | Chris Belcher and the openswap team | -Track these metrics for grant reporting: +## After the season -| Metric | Target | How to measure | -|--------|--------|----------------| -| Track completions | 80%+ of enrolled | Exercise submissions + capstone | -| PRs submitted | 1+ per participant | GitHub links | -| PRs merged | 30%+ of submitted | GitHub links | -| Ongoing contributors | 50%+ at 3 months | GitHub activity | -| Repeat cohorts | 2+ per year | Enrollment records | +- Run S12 as a sprint, then hold a retro: which sessions produced proof of work, which labs broke, and which PRs got reviewed +- Update [ISSUE_POOL.md](./ISSUE_POOL.md) and move merged/closed entries to the log +- Invite repeat contributors into the Review Club and the [fellowship](https://github.com/code-orange-dev/fellowships) pipeline +- Measure retention at 3, 6 and 12 months, per the program's [MEASUREMENT.md](https://github.com/code-orange-dev/code-orange-dev/blob/main/privacy-contributor-program/MEASUREMENT.md) --- diff --git a/privacy-track/ISSUE_POOL.md b/privacy-track/ISSUE_POOL.md index 4eb5df7..904abad 100644 --- a/privacy-track/ISSUE_POOL.md +++ b/privacy-track/ISSUE_POOL.md @@ -1,100 +1,125 @@ # Curated Issue Pool -**The single source of contribution targets for the Privacy Track.** Participants pick from here - never from random hunting across GitHub. This is the guardrail that lets us ship high volume without becoming noise to maintainers. +**The source of contribution targets for the Privacy Sessions.** Participants pick from here instead of hunting randomly across GitHub. That's the guardrail that keeps us useful to maintainers rather than noisy. -> **Rule:** If an issue isn't in this file (or just got verified by a curator), it is not a valid target for a session. No exceptions. +> **Rule:** if an issue isn't in this file (or a curator hasn't just verified it), it isn't a session target. ---- - -## How this pool stays healthy - -A curator (a tutor or experienced participant) keeps this list fresh. An issue earns a spot only if it passes all four checks: - -1. **Still open.** Verified open on GitHub within the last 7 days. -2. **Genuinely wanted.** Labeled `good-first-issue` / `help-wanted`, or explicitly confirmed welcome by a maintainer. Not a stale wishlist item, not something with an open PR already. -3. **Beginner-appropriate.** Scoped so someone could plausibly finish it in or shortly after a session. Docs, tests, examples, small fixes. -4. **Not already claimed.** Nobody else is mid-PR on it. - -Re-verify the whole pool **weekly**. Mark anything that goes stale as `RETIRED` (don't delete - the history is useful). Aim for **5–10 live issues per repo** at any time. +**Last full verification: 2026-09-26.** Re-verify weekly and on the morning of any session. --- -## The maintainer-DM sourcing playbook - -This is how we turn "we know a few maintainers" into the warmest possible issue pipeline - and into Code Orange's real long-term asset. - -**The goal:** a handful of maintainers who think of Code Orange as *"the program that sends me prepared contributors,"* and who occasionally point us at issues they'd actually love help on. +## Two kinds of target -**The opening DM (adapt per person):** -> Hey [name] - I run Code Orange's Bitcoin Privacy Track. We teach curious Bitcoiners and guide each one to a real, well-prepared PR (they run a build/test/CONTRIBUTING checklist before submitting - we're careful not to add noise). [repo] is one of the projects we contribute to. Are there a few issues you'd genuinely welcome outside help on - especially `good-first-issue`-type work? Happy to point our people only at things you actually want touched. +- **BUILD**: an open issue with no active PR. You can claim it (comment first) and work on it +- **TEST**: an issue that already has an open PR. Don't open a competing PR. Build the PR, run it, and post a useful test report if one's missing. For newcomers this is often the *better* contribution -**Why this works:** it leads with respect for their time, signals we have a quality floor, and asks them to *pull* work toward us rather than us pushing PRs at them. +## How an issue earns a spot -**After the first merge:** send a short thank-you and ask "anything else in this vein?" That loop is the whole game. +1. **Open**, verified within the last 7 days +2. **Wanted**: labeled `good first issue` / `help wanted`, or confirmed welcome by a maintainer +3. **Scoped**: plausible to finish in or shortly after a session +4. **Not claimed**: for BUILD, no assignee and no open PR. Otherwise it goes in as TEST -**Repos to open this conversation with first** (warmest, most beginner-friendly): **rust-bitcoin**, **Floresta**, then **BDK** and **Payjoin Dev Kit**. - -**Sourcing checklist for each repo:** -- [ ] Identify 1–2 active maintainers (recent commits / review activity). -- [ ] Send the opening DM. -- [ ] Read their CONTRIBUTING.md and link it in the pool entry. -- [ ] Subscribe to their `good-first-issue` label. -- [ ] Log their response and any issues they flag, below. +Stale entries get marked `RETIRED` (don't delete them; the history is useful). Aim for 3-6 live entries per repo. --- -## Live issues by repo - -> Curators: fill `#`, title, link, label, difficulty, and the maps-to session. Keep entries one line where possible. `STATUS`: LIVE / CLAIMED / RETIRED. +## Live pool + +### Silent Payments (S1-S3) + +| Issue | Type | Why it fits | Status | +|---|---|---|---| +| [shroud#132](https://github.com/CypherCommons/shroud/issues/132) Unit tests for the Rust BIP-352 scanner | BUILD | The SP lab teaches exactly what to test, and the official vectors are the test data. A previous attempt (#133) was closed, so read why first | LIVE | +| [shroud#156](https://github.com/CypherCommons/shroud/issues/156) SP UTXOs spent elsewhere never detected | BUILD | A real wallet bug; reproduce it on signet first. Community project (Chaitika) | LIVE | +| [bdk-sp#55](https://github.com/bitcoindevkit/bdk-sp/issues/55) False "bad tweak" when intermediate key sum hits zero | BUILD | This is lab vector 27, and the BIP fix already landed ([bips#2142](https://github.com/bitcoin/bips/pull/2142)) | LIVE | +| [rust-silentpayments#32](https://github.com/cygnet3/rust-silentpayments/issues/32) Add reference to code examples | BUILD | Docs, a gentle first PR | LIVE | +| [rust-silentpayments#109](https://github.com/cygnet3/rust-silentpayments/issues/109) Duplicate `calculate_ecdh_shared_secret()` | BUILD | Small refactor; ask the maintainer which copy should stay | LIVE | +| [danawallet#434](https://github.com/cygnet3/danawallet/issues/434) Button overlap in 3-button navigation | BUILD | Labeled `quick win`, Flutter UI | LIVE | +| [danawallet#403](https://github.com/cygnet3/danawallet/issues/403) Padding when keyboard is open | BUILD | Flutter UI, small | LIVE | +| [danawallet#466](https://github.com/cygnet3/danawallet/issues/466) Output-ambiguity recognition | BUILD (advanced) | Research + code. Only for people who finished the labels stretch | LIVE | +| [blindbit-oracle#58](https://github.com/setavenger/blindbit-oracle/issues/58) GetFullBlock drops outpoints | TEST | Open fix in [#60](https://github.com/setavenger/blindbit-oracle/pull/60): build it and test against signet | LIVE | +| [blindbit-oracle#36](https://github.com/setavenger/blindbit-oracle/issues/36) Add debug info endpoint | BUILD | Go, self-contained | LIVE | +| [bitcoin#36338](https://github.com/bitcoin/bitcoin/pull/36338) P2PKH pubkey extraction from malleated scriptSig | TEST / review | S1's Review Circle PR. Build it, run `bip352_tests` | LIVE | + +### Coin selection & wallet (S4-S5) + +| Issue | Type | Why it fits | Status | +|---|---|---|---| +| [bdk_wallet#543](https://github.com/bitcoindevkit/bdk_wallet/issues/543) Missing MAX_STANDARD_TX_WEIGHT check | TEST | Fix open in [#544](https://github.com/bitcoindevkit/bdk_wallet/pull/544) | LIVE | +| [bdk_wallet#46](https://github.com/bitcoindevkit/bdk_wallet/issues/46) Network consistency between genesis and descriptors | TEST | Fix open in [#520](https://github.com/bitcoindevkit/bdk_wallet/pull/520) | LIVE | +| [bdk_wallet#187](https://github.com/bitcoindevkit/bdk_wallet/issues/187) Return feerate from tx construction | TEST | Fix open in [#380](https://github.com/bitcoindevkit/bdk_wallet/pull/380) | LIVE | + +### Payjoin (S6) + +| Issue | Type | Why it fits | Status | +|---|---|---|---| +| [rust-payjoin#865](https://github.com/payjoin/rust-payjoin/issues/865) `cargo doc` improvements (tracking) | BUILD | Many small doc items; claim one item in a comment | LIVE | +| [rust-payjoin#551](https://github.com/payjoin/rust-payjoin/issues/551) Preserve privacy for >2-output txs | BUILD (advanced) | Real privacy logic. A previous attempt (#1837) closed, so read it first | LIVE | +| [rust-payjoin#1300](https://github.com/payjoin/rust-payjoin/issues/1300) .gitignore organization | - | A PR exists on a fork; check with the maintainers before touching it | CHECK | + +### Light clients & nodes (S7) + +| Issue | Type | Why it fits | Status | +|---|---|---|---| +| [Floresta#799](https://github.com/getfloresta/Floresta/issues/799) Document all floresta-cli RPC endpoints | BUILD | Tracking issue with several merged PRs; pick an endpoint that's still undocumented | LIVE | +| [Floresta#1014](https://github.com/getfloresta/Floresta/issues/1014) Deny dangerous `as` casts | BUILD | Rust hygiene, split into small PRs | LIVE | +| [Floresta#1308](https://github.com/getfloresta/Floresta/issues/1308) config.toml parsed three times | - | A fork PR exists; ask before starting | CHECK | +| [kyoto](https://github.com/2140-dev/kyoto/issues) | - | No beginner-labeled issues right now. Use Kyoto for reading and testing, and ask a maintainer (sourcing playbook below) | SOURCING | + +### Network privacy (S8) + +| Issue | Type | Why it fits | Status | +|---|---|---|---| +| [peer-observer#182](https://github.com/peer-observer/peer-observer/issues/182) Metric for unsolicited/unannounced txs | BUILD | `good first issue`, a clear spec | LIVE | +| [peer-observer#336](https://github.com/peer-observer/peer-observer/issues/336) log-extractor: parse debug.log | BUILD | `help wanted`, bigger but well-bounded | LIVE | +| [peer-observer#396](https://github.com/peer-observer/peer-observer/issues/396) Sequence diagrams in websocket-tool | BUILD | Front-end-ish, visual | LIVE | +| Bitcoin Core private-broadcast follow-ups ([#34322](https://github.com/bitcoin/bitcoin/pull/34322), [#34533](https://github.com/bitcoin/bitcoin/pull/34533), [#36330](https://github.com/bitcoin/bitcoin/pull/36330)) | TEST / review | Test with Tor/I2P on your platform, report exact steps | LIVE | + +### CoinJoin, swaps, Lightning, ecash (S9-S11) + +| Issue | Type | Why it fits | Status | +|---|---|---|---| +| [joinmarket-clientserver good first issues](https://github.com/JoinMarket-Org/joinmarket-clientserver/issues?q=is%3Aopen+label%3A%22good+first+issue%22) | BUILD | 20+ labeled, but many are years old. Confirm an issue is still wanted before starting | SOURCING | +| [openswap#1021](https://github.com/citadel-foss/openswap/issues/1021) Derive minimum swap amount instead of hardcoding | BUILD | Scoped; confirm with maintainers | CHECK | +| [openswap#1037](https://github.com/citadel-foss/openswap/issues/1037) Human-readable maker names | BUILD | Scoped UX feature | CHECK | +| LDK / ldk-node / Core Lightning offers, cdk, Fedimint | - | No verified beginner targets yet. Source via maintainers before S10/S11 | SOURCING | -### rust-bitcoin 🟢 -CONTRIBUTING: _link here_ · Maintainer contact: _DM status here_ +--- -| # | Title | Link | Label | Maps to | Status | -|---|---|---|---|---|---| -| _tbd_ | _e.g. clarify docs on Transaction fields_ | _url_ | good-first-issue | F1, F2, T3 | LIVE | +## The maintainer-DM sourcing playbook -### Floresta 🟢 -CONTRIBUTING: _link here_ · Maintainer contact: _DM status here_ +This is how "we know a few maintainers" becomes a warm issue pipeline, and it's Code Orange's real long-term asset. -| # | Title | Link | Label | Maps to | Status | -|---|---|---|---|---|---| -| _tbd_ | _e.g. improve node setup docs_ | _url_ | good-first-issue | N2, N3 | LIVE | +**The goal:** a handful of maintainers who think of Code Orange as *"the program that sends me prepared contributors"* and occasionally point us at work they'd actually like help with. -### BDK (bdk_wallet) 🟡 -CONTRIBUTING: _link here_ · Maintainer contact: _DM status here_ +**The opening DM (adapt it per person):** +> Hey [name] - I help run Code Orange's Bitcoin Privacy Sessions. We teach [topic] hands-on, and participants build and test real PRs before they ever open their own (they run a build/test/CONTRIBUTING checklist first - we're careful not to add noise). Are there a few issues, or PRs that need testing, where outside help would genuinely be welcome? We'll only point people at things you actually want touched. -| # | Title | Link | Label | Maps to | Status | -|---|---|---|---|---|---| -| _tbd_ | _e.g. add tests for coin selection_ | _url_ | help-wanted | T1, T2 | LIVE | +**Why it works:** it leads with respect for their time, signals that we have a quality floor, and asks them to *pull* work toward us instead of us pushing PRs at them. Offering *testing* is often the most welcome thing we can offer. -### Payjoin Dev Kit (rust-payjoin) 🟡 -CONTRIBUTING: _link here_ · Maintainer contact: _DM status here_ +**After the first merge or useful test report:** send a short thank-you and ask "anything else in this vein?" That loop is the whole game. -| # | Title | Link | Label | Maps to | Status | -|---|---|---|---|---|---| -| _tbd_ | _e.g. expand async example_ | _url_ | good-first-issue | C1, C2 | LIVE | - -### Silent Payments tooling 🟡 -CONTRIBUTING: _link here_ · Maintainer contact: _DM status here_ +**Priority for this season:** Silent Payments (cygnet3, setavenger, nymius, the Shroud team), Kyoto, openswap, and the LN/ecash repos for S10-S11. -| # | Title | Link | Label | Maps to | Status | -|---|---|---|---|---|---| -| _tbd_ | _e.g. document address derivation_ | _url_ | good-first-issue | S1, S2 | LIVE | +**Per-repo checklist:** +- [ ] Identify 1-2 active maintainers (recent commits / reviews) +- [ ] Send the opening DM and log the response below +- [ ] Link their CONTRIBUTING.md in the table +- [ ] Watch their `good first issue` / `help wanted` labels -### Kyoto 🟡 -CONTRIBUTING: _link here_ · Maintainer contact: _DM status here_ +### Maintainer contact log -| # | Title | Link | Label | Maps to | Status | -|---|---|---|---|---|---| -| _tbd_ | _e.g. clarify filter-sync docs_ | _url_ | good-first-issue | N1, S2 | LIVE | +| Date | Repo | Maintainer | Response / issues flagged | +|---|---|---|---| +| | | | | --- -## Retired / merged log +## Outcomes log -Keep a running record - it's how we report "merged PRs" and spot which repos are working. +Record everything here: merged PRs, useful test reports, reproduced bugs. It's how we report proof of work and spot which repos are working for us. -| Date | Repo | # | Contributor | Outcome (MERGED/CLOSED) | Notes | +| Date | Repo | Link | Contributor | Type (PR / test / review / repro) | Outcome | |---|---|---|---|---|---| -| _tbd_ | | | | | | +| | | | | | | diff --git a/privacy-track/PR_CHECKLIST.md b/privacy-track/PR_CHECKLIST.md index 59b6495..e53897a 100644 --- a/privacy-track/PR_CHECKLIST.md +++ b/privacy-track/PR_CHECKLIST.md @@ -2,21 +2,21 @@ **Run this before every pull request goes upstream. No exceptions.** -This is the ten-minute step that turns a maybe-PR into a merge - and the reason maintainers welcome Code Orange instead of muting it. We ship volume; this is how we keep that volume *respected*. A tutor should make sure every participant runs it before hitting "Create pull request." +This is the ten-minute step that turns a maybe-PR into a merge, and it's the reason maintainers welcome Code Orange instead of muting it. A facilitator should make sure every participant runs it before hitting "Create pull request" or "Comment". --- ## Before you start coding -- [ ] The issue is from [`ISSUE_POOL.md`](./ISSUE_POOL.md) and marked LIVE (not random, not already claimed). +- [ ] The issue is from [`ISSUE_POOL.md`](./ISSUE_POOL.md), marked LIVE and type BUILD (not random, not already claimed). - [ ] You commented on the issue (or checked it's unassigned) so two people don't do the same work. - [ ] You read the repo's **CONTRIBUTING.md**. Each repo has its own rules - follow theirs over any general habit. ## Before you submit -- [ ] **It builds.** The project compiles locally with no new errors (`cargo build`). -- [ ] **Tests pass.** The existing test suite is green (`cargo test`). If you changed behavior, you added or updated a test. -- [ ] **Formatted and linted.** You ran the repo's formatter and linter (usually `cargo fmt` and `cargo clippy`) and fixed what they flagged. +- [ ] **It builds.** The project compiles locally with no new errors (`cargo build`, `go build`, `cmake --build`, `flutter build`... whatever the repo uses). +- [ ] **Tests pass.** The existing test suite is green (`cargo test`, `go test ./...`, `ctest`...). If you changed behavior, you added or updated a test. +- [ ] **Formatted and linted.** You ran the repo's own formatter and linter (e.g. `cargo fmt` + `cargo clippy`, `gofmt`, `clang-format`) and fixed what they flagged. - [ ] **Scope is tight.** The PR does *one thing* - the thing in the issue. No drive-by changes, no reformatting unrelated files. - [ ] **It links the issue.** The PR description says `Closes #123` (or `Refs #123`). - [ ] **Commits are clean.** Clear messages; follows the repo's commit-message style if it has one. @@ -26,7 +26,32 @@ This is the ten-minute step that turns a maybe-PR into a merge - and the reason - [ ] Watch for review comments and respond promptly and politely. - [ ] Expect change requests - that's normal and is part of contributing, not a rejection. -- [ ] When it merges (or closes), log it in the ISSUE_POOL merged log. +- [ ] When it merges (or closes), log it in the ISSUE_POOL outcomes log. + +--- + +## Before you post a review or test report + +Reviews and test reports are real contributions, and they're also the easiest way to add noise. Before you comment on someone else's PR: + +- [ ] **You built and ran it**, or you're clear that your comment is conceptual only. +- [ ] **You read the whole thread.** Your point hasn't already been made or answered. +- [ ] **It's specific.** Platform, commit hash, exact commands, what you expected, what happened. "Tested ACK " plus the steps you ran beats a paragraph of praise. +- [ ] **It's yours.** You understand everything you're saying and could defend it in a follow-up. No pasted AI output, no comments written by a facilitator for you. +- [ ] **It's worth a maintainer's minute.** If you're unsure, discuss it in the Code Orange channel first. Not posting is a fine outcome. + +### Test report template + +``` +Tested on , . + +Steps: +1. ... +2. ... + +Result: + +``` --- @@ -44,9 +69,9 @@ Closes # ## How I tested it -- [ ] `cargo build` passes -- [ ] `cargo test` passes -- [ ] `cargo fmt` / `cargo clippy` clean +- [ ] builds locally +- [ ] tests pass (which ones?) +- [ ] formatter / linter clean ## Notes for the reviewer diff --git a/privacy-track/README.md b/privacy-track/README.md index e74ec0d..69548b0 100644 --- a/privacy-track/README.md +++ b/privacy-track/README.md @@ -1,435 +1,107 @@ -# Privacy Track +# Code Orange · Bitcoin Privacy Track -**Season One starts October/November - 12 biweekly 90-min sessions. [Full session guide with facilitator prep →](./SESSIONS.md)** +**The Privacy Sessions: 12 biweekly, 90-minute, drop-in sessions. Season One opens with Silent Payments.** [Full session guide with facilitator prep →](./SESSIONS.md) -**Month 1** -- Session 1: Chain Analysis -- Session 2: Wallet Fingerprinting - -**Month 2** -- Session 3: Coin Selection & UTXO Management -- Session 4: Silent Payments - -**Month 3** -- Session 5: Payjoin -- Session 6: Light Clients (Floresta & Kyoto) - -**Month 4** -- Session 7: P2P Privacy & Node Fingerprinting -- Session 8: ASmap & Network Attacks - -**Month 5** -- Session 9: CoinJoin (JoinMarket) -- Session 10: OpenSwap (formerly CoinSwap) - -**Month 6** -- Session 11: Lightning Privacy -- Session 12: Contribution Sprint - ---- - -# Code Orange - Bitcoin Privacy Track - -**A drop-in, drop-out program where every session ends with a real contribution to a Bitcoin privacy project.** - -You do not have to attend all of this. There is no "start" and no "finish." Each session stands on its own, teaches one privacy topic completely, and guides you to open a real pull request against a real open-source privacy repository before you leave. Come to one session. Come to ten. Come when a topic interests you, skip when it doesn't. The only thing every session has in common is the goal: **you contribute.** +| Month | Sessions | +|---|---| +| 1 | **S1** Silent Payments I: One Address, No Trail · **S2** Silent Payments II: Finding Your Money | +| 2 | **S3** Silent Payments III: Review Club · **S4** Think Like the Adversary | +| 3 | **S5** Coin Selection & Change · **S6** Payjoin | +| 4 | **S7** Light Clients & Your Own Node · **S8** The Network Sees You (Tor, BIP324, private broadcast, ASmap) | +| 5 | **S9** CoinJoin & OpenSwap · **S10** Lightning Privacy | +| 6 | **S11** Ecash: Cashu & Fedimint · **S12** Contribution Sprint | --- ## Why this track exists -Privacy on Bitcoin matters for the same reason it matters anywhere else. Someone accepting bitcoin for their work, running a small shop, donating to a cause, or saving for the future expects the same everyday financial privacy they'd get from a bank - their landlord shouldn't see their balance, their employer shouldn't see their donations, and a stranger shouldn't be able to look up how much they hold and target them for it. +Bitcoin's base layer is public by default. Most of the privacy failures people actually hit aren't in the protocol. They're in **wallet behavior**, **how transactions get built**, and **how they get broadcast**: reusing an address, picking change badly, a wallet's fingerprint, a light client that tells a server every address you own, a broadcast that leaks your IP. Those are software problems, and open-source contributors can fix them. -But Bitcoin's base layer is public by default. Every transaction is permanently visible to anyone. Most of the privacy weaknesses people actually hit aren't in the protocol - they're in **wallet behavior** and **how transactions get built**. That's exactly the kind of thing open-source contributors can fix. Reusing an address, picking change badly, leaking which wallet you use through subtle fingerprints, broadcasting in a way that reveals your IP - these are software problems, and software problems get solved by people sending pull requests. +This track teaches those problems one at a time, with the code open on screen, and connects every topic to a live project where the fix is being written. -That's what this track is for. Not to talk about privacy - to **ship** privacy. +## Why start with Silent Payments ---- +Because it's landing right now. Bitcoin Core merged the BIP352 core logic in September 2026 ([#35301](https://github.com/bitcoin/bitcoin/pull/35301)), libsecp256k1 v0.8.0 ships a `silentpayments` module, and wallets like Dana, Cake, BlueWallet, Sparrow and our own community's [Shroud](https://github.com/CypherCommons/shroud) are shipping support. Sending and receiving in Core are in review. Participants can build the protocol from scratch (S1-S2) and then review the real implementation while it's still open (S3). -## How this track works +## How it works -- **Online and global.** Sessions run remotely. Anyone, anywhere, any timezone we can manage. You need a laptop and curiosity. -- **Standalone sessions.** Every session is self-contained. You will never be lost because you "missed last week." Each one re-teaches the small amount of context it needs. -- **Drop in, drop out.** Join any session. Leave any session. Come back next month. No attendance requirement, no sequence to follow, no graduation gate. Your progress is measured in merged PRs, not in seats filled. -- **Every session ends in a contribution.** You don't leave with notes. You leave with a pull request open against a real repository - picked from our curated issue pool (see below), run through a quick quality checklist, and submitted upstream. -- **Low barrier on purpose.** Session one can be your first day writing Rust. We've designed the contribution targets so a curious Bitcoiner can ship something real immediately and grow from there. +- **Online, global, drop-in.** Every session stands alone. You'll never be lost because you missed last time +- **Hands-on every time.** You build or run something on your own machine: a Silent Payments sender graded against the official BIP352 vectors, a node, a payjoin, a swap +- **Review-first.** Every session reads a real open PR together. We learn to build, test and explain other people's work, which is how contributors earn trust +- **Proof of work, not attendance.** You leave with a concrete next step: a finished lab, a tested PR, a reproduced bug, a doc fix, or a PR from the [curated pool](./ISSUE_POOL.md) ---- +## Where it fits -## A note for tutors +The Privacy Sessions are the open, drop-in layer of Code Orange's [Privacy Contributor Program](https://github.com/code-orange-dev/code-orange-dev/tree/main/privacy-contributor-program): -**You do not need to be a deep technical developer to teach this track.** You need to be a curious Bitcoiner who wants to build on Bitcoin. That's it. This track is as much a teaching ground for *you* as for the people in the room - you will learn this material by preparing it and by working through the exercises alongside everyone else. +``` +Workshops & meetups ──> The Privacy Sessions (this track) ──> Privacy Review Club ──> Rotations & residency ──> Fellowship / grant + curious learn + build + first review regular reviewer one project, deep independent +``` -Here is how to teach it well: +Anyone can join the sessions. People who keep coming back and produce proof of work get invited into the Review Club and the contributor pathway. See [fellowships](https://github.com/code-orange-dev/fellowships). -1. **Do the reading and the exercise yourself first.** Every session below has a "Tutor preparation" block with plain-language explanations, an analogy you can use, the questions people will ask, and how long to budget for prep. Work through the build *before* the session. If you can do it, you can teach it. -2. **Teach the WHY, not the HOW.** Your job is not to be the smartest coder in the room. Your job is to explain *why this privacy problem matters* and *where in the code it lives*. The repo's own documentation handles the HOW. Point people at it. -3. **Be honest when you don't know.** "I don't know - let's find out together" is the most powerful thing you can say. It models exactly the behavior a good open-source contributor needs. Look it up live. Read the code together. Ask in the repo's chat. -4. **Learn alongside the room.** You are not above the participants, you are one session ahead of them. That's enough. The best tutors here are people who started as participants. -5. **Protect the relationship with maintainers.** This is the one rule you must not bend. We contribute *from a curated issue pool* and we run *a quality checklist* before anything goes upstream. Read `PR_CHECKLIST.md` and make sure every participant runs it. A program that sends sloppy pull requests gets ignored; a program that sends prepared contributors gets welcomed. You are the guardian of that reputation. - -If you can host a call, do the homework one session ahead, and stay curious, you can run this track. - ---- +## Labs -## How contributing works - read this before any session +The Python labs run offline with plain Python 3.8+ and need no installs. The payjoin and network labs are step-by-step guides using real software. -We optimize for **volume of merged pull requests** - lots of real, accepted contributions. But there's a trap: the maintainers of these privacy repos are mostly unpaid and time-starved, and the privacy world is small. If we flood them with random, low-quality PRs, "Code Orange" becomes a name they associate with noise, and doors close. So we keep volume high *and* protect the relationship with three rules: - -1. **Pick from the curated pool, never the wild.** All contribution targets come from [`ISSUE_POOL.md`](./ISSUE_POOL.md) - a living list of issues we've verified are still open, genuinely wanted, and beginner-appropriate. Don't go hunting for random issues to "fix." -2. **Run the checklist before you submit.** Every PR passes [`PR_CHECKLIST.md`](./PR_CHECKLIST.md): it builds locally, tests pass, it links the issue, and it follows the repo's own CONTRIBUTING guide. Ten minutes that turns a maybe-PR into a merge. -3. **Submit directly upstream - but prepared.** Once it clears the checklist, you open the PR yourself against the real repo and engage with the maintainer's review like a real contributor. Because you are one. - -The result: high throughput, defensible quality, and maintainers who are glad to see us. - ---- - -## The repositories we contribute to - -All Rust. All central to base-layer privacy. Ordered roughly easiest-to-enter first. - -| Repo | What it is | Why privacy cares | Entry level | -|---|---|---|---| -| **rust-bitcoin** | The foundational Bitcoin library in Rust | Everything else is built on it; tx parsing, addresses, script all live here | 🟢 Easiest - strong good-first-issue culture | -| **Floresta** | A Utreexo-based full node in Rust | Lets people run their own node cheaply = no third party watching their wallet | 🟢 Welcoming, active | -| **BDK (bdk_wallet)** | The Bitcoin Dev Kit wallet library | Coin selection, change handling, descriptors - where wallet privacy is won or lost | 🟡 Moderate | -| **Payjoin Dev Kit (rust-payjoin)** | Library for Payjoin collaborative transactions | Breaks the assumption that all transaction inputs belong to one person | 🟡 Moderate | -| **Silent Payments tooling** | Libraries implementing BIP352 | Lets you receive to a single static address with zero address reuse | 🟡 Moderate / advanced | -| **Kyoto** | A BIP157/158 compact-block-filter light client in Rust | Light clients that don't leak which addresses are yours to a server | 🟡 Moderate / advanced | - ---- - -## The privacy problems we're solving - -Every session maps to a concrete, well-known weakness. In plain language: - -| The problem | What it means for a normal person | Where we fix it | +| Lab | Session | What you build | |---|---|---| -| **Address reuse** | Reusing an address turns your whole history into one searchable profile | Silent Payments sessions | -| **Common-input-ownership** | Spending two coins together usually proves the same person owns both | Heuristics + collaborative-tx sessions | -| **Change detection** | Analysts can often guess which output is your change, then follow your money | Coin selection sessions (BDK) | -| **Wallet fingerprinting** | The *way* your wallet builds a transaction can reveal which wallet - and sometimes which user | Fingerprinting sessions (rust-bitcoin/BDK) | -| **Light-client leakage** | "Light" wallets often tell a server exactly which addresses are yours | Light-client sessions (Kyoto) | -| **Node/IP exposure** | Broadcasting a transaction can leak the IP it came from | P2P privacy + node sessions (Floresta) | -| **Network-level deanonymization** | An attacker controlling network routes can isolate or watch your node | ASmap + eclipse-attack sessions | -| **Linking on receipt** | One static donation address links every donor and every payment together | Silent Payments sessions | - ---- +| [silent-payments](./labs/silent-payments/) | S1, S2 | A BIP352 sender and scanner graded against the 28 official test vectors, plus a scanning-cost benchmark | +| [chain-analysis](./labs/chain-analysis/) | S4 | The core heuristics (CIOH, change detection, clustering) on sample transactions | +| [coin-selection](./labs/coin-selection/) | S5 | Four selection algorithms, scored on fees vs privacy | +| [payjoin](./labs/payjoin/) | S6 | An async payjoin on regtest, then catch the common-input heuristic lying (guide: needs Docker + Rust) | +| [compact-block-filters](./labs/compact-block-filters/) | S7 | A simplified BIP158 filter: Golomb-Rice coding, matching, false positives | +| [network-privacy](./labs/network-privacy/) | S8 | BIP324 in Wireshark, `-privatebroadcast`, ASmap peer diversity (guide: needs Bitcoin Core 31 + Tor) | -## The sessions +```bash +git clone https://github.com/code-orange-dev/curriculum +cd curriculum/privacy-track/labs/silent-payments +python3 run_vectors.py send +``` -These are **modules, not a sequence.** Pick whatever pulls you in. Each lists who it's for, what you'll learn in plain terms, a full tutor-preparation block, what you'll build, and the contribution you'll make. +## For facilitators -A good first session for anyone brand new is **F1**. After that, follow your curiosity. +**You don't need to be a deep technical developer to host.** You need to be curious, one session ahead of the room, and honest when you don't know. "Let's find out together" models exactly what a good contributor does. ---- - -### Foundations - -#### F1 · Set up your toolchain and ship your first contribution -**Who it's for:** Total newcomers. Possibly your first day writing Rust. -**What you'll learn (plain):** How to clone a Bitcoin repo, build it, run its tests, and open a pull request. The mechanics of contributing - fork, branch, commit, PR, respond to review. - -**Tutor preparation** *(prep: ~90 min the first time, ~20 min after)* -- **The concept in plain words:** Contributing to open source is just five moves: copy the project (fork), make a workspace (branch), change something, save it with a note (commit), and offer it back (pull request). Everything else is detail. -- **Analogy to use:** It's like suggesting an edit to a shared recipe. You photocopy the cookbook (fork), scribble your improvement on your copy (branch + commit), and hand it to the author saying "want this?" (PR). They might say yes, or ask you to change it first. -- **Common questions and answers:** - - *"Do I need to know Rust?"* - Not today. Today we fix a doc, a comment, or a failing-to-be-clear error message. You'll write Rust in later sessions. - - *"What if my PR gets rejected?"* - Totally normal and not a failure. Maintainers often ask for changes. That conversation *is* contributing. - - *"Will I break Bitcoin?"* - No. You're proposing a change to a copy. Nothing you do touches the real network or gets merged without review. -- **How to run it:** Screen-share the whole flow once, slowly, on rust-bitcoin. Then everyone does it on a curated `good-first-issue` (docs/typo/clarity issues are ideal here). Budget half the session for "my build is failing" - that's the real lesson. -- **When you get stuck:** rust-bitcoin's CONTRIBUTING.md and its chat are excellent. Reading them together is part of the teaching. - -**Build:** Fork, build, and test rust-bitcoin (or Floresta) locally. -**Contribute:** Pick a `good-first-issue` from the pool (a docs/clarity/test fix) and open your first PR. - ---- - -#### F2 · Read a Bitcoin transaction byte by byte -**Who it's for:** Anyone who wants to actually *see* where privacy leaks happen. -**What you'll learn (plain):** What's inside a transaction - inputs, outputs, amounts, scripts, locktime - and which of those fields quietly leak information. - -**Tutor preparation** *(prep: ~60 min)* -- **The concept in plain words:** A transaction is just a list of "coins I'm spending" (inputs) and "where the money goes" (outputs), plus some settings. Every one of those fields is public forever, and several of them accidentally say something about you. -- **Analogy to use:** A transaction is like a cheque you photocopy and pin to a public noticeboard for all time. Most people only think about the amount - but the handwriting, the bank, the way you fill in the date, all reveal things too. -- **Common questions:** - - *"Why are amounts public?"* - Bitcoin needs everyone to verify no money was invented. Public amounts are the price of not trusting a central bank. - - *"What's a 'script'?"* - The little rulebook attached to each coin saying how it can be spent. Different wallets write it differently - that's a fingerprint. -- **How to run it:** Decode a real transaction together using rust-bitcoin's parsing. Point at each field and ask "what could someone learn from this?" -- **When you get stuck:** rust-bitcoin's `Transaction` docs label every field. - -**Build:** A tiny Rust program using rust-bitcoin that decodes a raw transaction and prints each field. -**Contribute:** Improve the docs or examples for transaction parsing in rust-bitcoin - clarity fixes here are genuinely valued. Pick from the pool. - ---- - -### Transaction privacy & wallet behavior - -#### T1 · The five privacy heuristics -**Who it's for:** Anyone. No code required to understand it; light code to contribute. -**What you'll learn (plain):** The five rules of thumb chain-analysis firms use to deanonymize people - and how to recognize each one. - -**Tutor preparation** *(prep: ~75 min)* -- **The five heuristics in plain words:** - 1. **Common-input-ownership** - if a transaction spends several coins at once, they're probably all owned by the same person. (Like paying with a fistful of bills from the same pocket.) - 2. **Address reuse** - using one address repeatedly ties all those payments into one identity. (Like using one email address for everything.) - 3. **Round amounts** - a payment of exactly 0.05 BTC is probably the *payment*; the weird leftover is probably your *change*. - 4. **Change detection** - combine the above to guess which output comes back to you, then keep following it. - 5. **Wallet fingerprinting** - the style in which a transaction is built points to a specific wallet. -- **Analogy to use:** Imagine watching people at a market through binoculars. You can't hear them, but you notice who pulls cash from the same wallet, who always uses the same stall, who gets exact change back. You'd learn a lot. That's chain analysis. -- **Common questions:** - - *"Isn't this just for catching criminals?"* - No. The same techniques let anyone - a stalker, a thief, a nosy employer - profile an ordinary person. Privacy is normal; surveillance is the anomaly. -- **How to run it:** Walk through one real transaction and apply each heuristic out loud as a group. Let people *feel* how easy it is. -- **When you get stuck:** These are conceptual; no deep code needed to teach. - -**Build:** Annotate a real transaction identifying which heuristics apply. -**Contribute:** Many privacy libraries have docs explaining these heuristics. Improve or extend that explanatory documentation in BDK or rust-bitcoin - pick from the pool. - ---- - -#### T2 · Coin selection and change - how wallets leak -**Who it's for:** People ready for a bit of wallet logic. -**What you'll learn (plain):** How a wallet decides which coins to spend, and how that choice either protects or exposes you. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** When you spend, your wallet picks from the coins you hold (your "UTXOs"). Picking badly - grabbing more coins than needed, or producing tell-tale change - hands analysts a map. Good coin selection is quiet. -- **Analogy to use:** It's like paying with coins from a jar. If you tip the whole jar out every time, everyone sees exactly what you have and how much you got back. A careful person picks just enough. -- **Common questions:** - - *"Why not always pick one coin?"* - Sometimes you can't; the amounts don't line up. The art is choosing to minimize what you reveal. - - *"What's a 'change output'?"* - Coins almost never match the price exactly, so the wallet sends the leftover back to you. That leftover is the juiciest thing for an analyst. -- **How to run it:** Read BDK's coin-selection code together. You don't need to understand every line - find *where the decision is made* and discuss what each strategy reveals. -- **When you get stuck:** BDK's docs describe its coin-selection algorithms by name; read them as a group. - -**Build:** Run BDK's coin selection on sample wallets and observe the change behavior. -**Contribute:** Tests and documentation around coin selection in BDK. Pick a pool issue. - ---- - -#### T3 · Wallet fingerprinting -**Who it's for:** The curious. Moderate. -**What you'll learn (plain):** The subtle "tells" - input ordering, locktime, version numbers, fee-bumping signals - that reveal which wallet built a transaction. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** Two wallets can make a valid transaction in slightly different ways - the order they list things, a setting they flip, a number they pick. None of it affects whether it works, but it acts like handwriting: it identifies the author. -- **Analogy to use:** Two people can write the same sentence, but their handwriting, spacing, and how they cross their sevens give them away. Wallets have handwriting too. -- **Common questions:** - - *"Why does ordering matter?"* - There's a standard (BIP69) for ordering inputs and outputs neutrally. A wallet that *doesn't* follow it stands out. - - *"What's nLockTime/RBF?"* - Settings about when/whether a tx can be replaced. The specific values a wallet chooses are a fingerprint. -- **How to run it:** Compare transactions from two wallets side by side. Spot the differences as a group - that *is* fingerprinting. -- **When you get stuck:** rust-bitcoin exposes all these fields; the BIP69 spec is short and readable. - -**Build:** A script that flags fingerprintable traits in a set of transactions. -**Contribute:** Documentation or test coverage for transaction-construction fields in rust-bitcoin. Pick from the pool. - ---- +1. **Do the hands-on yourself first.** Every session in [SESSIONS.md](./SESSIONS.md) has a prep line. If you can do it, you can host it +2. **Teach the why, then show where it lives in code.** The repo's own docs handle the how +3. **Protect the relationship with maintainers.** Contributions come from the [curated pool](./ISSUE_POOL.md) and pass the [PR checklist](./PR_CHECKLIST.md). Review comments go upstream only when they're genuinely useful. The privacy world is small, and a reputation for noise closes doors for everyone after us -### Collaborative transactions - -#### C1 · Payjoin fundamentals (BIP78 / BIP77) -**Who it's for:** Anyone curious about breaking chain-analysis assumptions. -**What you'll learn (plain):** How Payjoin lets the sender *and* receiver both put inputs into one transaction, quietly destroying the "all inputs are one owner" assumption. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** Normally one transaction = one payer's coins. Payjoin has the *receiver* also add a coin of their own. Now the common-input-ownership heuristic is simply wrong - and analysts can't tell a Payjoin apart from a normal payment, so it poisons their assumptions for everyone. -- **Analogy to use:** Imagine splitting a restaurant bill where both people put cash on the table into one pile before paying. An onlooker can no longer assume all the cash came from one wallet. -- **Common questions:** - - *"Does the receiver pay more?"* - No, the amounts net out; they just contribute an input that comes back to them. - - *"BIP78 vs BIP77?"* - Two versions of the coordination. BIP77 (async) doesn't require both parties online at once. That's the newer, more practical direction. -- **How to run it:** Read Payjoin Dev Kit's README and trace the sender/receiver roles. Don't aim to implement it live - aim to understand the dance. -- **When you get stuck:** Payjoin Dev Kit's docs and example code are the reference. - -**Build:** Run a Payjoin example transaction from the Payjoin Dev Kit examples. -**Contribute:** Docs, examples, or test fixes in rust-payjoin. Pick from the pool. - ---- - -#### C2 · Building with Async Payjoin -**Who it's for:** People who did C1 or already grasp Payjoin. -**What you'll learn (plain):** Why "both parties online at once" is a real-world blocker, and how async Payjoin (BIP77) removes it. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** The original Payjoin needs sender and receiver online simultaneously - awkward for a shop or a donation page. Async Payjoin uses a relay so the two sides can coordinate without being online together. -- **Analogy to use:** Instead of needing both people on a live phone call, they leave messages in a shared mailbox and pick them up whenever they're around. -- **Common questions:** - - *"Is the relay a trusted third party?"* - It coordinates but is designed not to learn or control the funds. Discuss the trust boundaries - this is a great real conversation. -- **How to run it:** Walk the async flow in the Payjoin Dev Kit. Identify each message that crosses the relay. -- **When you get stuck:** The rust-payjoin async examples. - -**Build:** Trace an async Payjoin flow end to end using the library's examples. -**Contribute:** Pick an async-Payjoin-related pool issue (often docs/tests/examples). - ---- - -#### C3 · CoinJoin and JoinMarket concepts -**Who it's for:** The conceptually curious. Light contribution. -**What you'll learn (plain):** How many people combining one big transaction breaks the link between who put money in and who took it out. - -**Tutor preparation** *(prep: ~75 min)* -- **The concept in plain words:** A CoinJoin is one transaction with many participants, all with equal-sized outputs, so you can't tell whose input maps to whose output. JoinMarket adds a marketplace so makers and takers can find each other. -- **Analogy to use:** Twenty people each drop an identical envelope of cash into a box, shake it, and each takes one identical envelope out. You can't match who put in which to who took which. -- **Common questions:** - - *"Why equal amounts?"* - Unequal amounts would let you match by size. Equality is what makes the mix work. -- **How to run it:** Conceptual walk-through plus reading project docs. Focus on the *why*. -- **When you get stuck:** Stay at the conceptual level; this session is about understanding, with documentation-level contributions. - -**Build:** Diagram a CoinJoin and explain why the linkage breaks. -**Contribute:** Documentation improvements to a relevant collaborative-transaction project. Pick from the pool. - ---- - -#### C4 · CoinSwap concepts -**Who it's for:** Those who enjoyed C3. -**What you'll learn (plain):** How swapping coins with someone else - so the coin you end up with has a totally different history - defeats following the money. - -**Tutor preparation** *(prep: ~75 min)* -- **The concept in plain words:** Instead of mixing in one visible transaction, two people swap coins in a way that, on-chain, looks like two ordinary unrelated payments. The history trail is cut. -- **Analogy to use:** You and a stranger discreetly trade identical-value gift cards. Anyone tracking your original card now follows a card that was never yours. -- **Common questions:** - - *"How is this different from CoinJoin?"* - CoinJoin is one obvious group transaction; CoinSwap aims to look like normal, separate payments. Harder to even detect. -- **How to run it:** Conceptual. Read the CoinSwap design notes together. -- **When you get stuck:** Keep it conceptual; contributions here are docs/tests. - -**Build:** Diagram a CoinSwap vs. a CoinJoin and contrast their on-chain footprints. -**Contribute:** Docs/test contribution to a relevant project from the pool. - ---- - -### Receiving privately - Silent Payments - -#### S1 · Silent Payments fundamentals (BIP352) -**Who it's for:** Anyone who's ever posted a static donation address. -**What you'll learn (plain):** How you can publish *one* unchanging address and still receive every payment to a *different*, unlinkable on-chain address. - -**Tutor preparation** *(prep: ~2 hrs - the math is the meat)* -- **The concept in plain words:** Normally a static address links every payment to it. Silent Payments use a shared-secret trick: the sender combines your public address with their own key to compute a fresh, unique address only you can detect and spend. You publish one thing; the chain shows many unrelated things. -- **Analogy to use (the key one):** Mixing paint. You each have a secret color. When the sender mixes their secret with your public color, they get a specific shade. You can recreate that exact shade because you know your own secret - but no onlooker can, because they're missing one of the colors. Each payment produces a different shade. -- **Common questions:** - - *"Doesn't the sender need to talk to me?"* - No interaction needed. They derive it from your published address alone. That's the magic. - - *"What's ECDH?"* - The "shared secret from two key pairs" math behind the paint-mixing. You don't need the equations to teach the idea - use the analogy. - - *"What's the catch?"* - The receiver has to *scan* the chain to find payments meant for them. That's a real cost, and it's why light-client work (Kyoto) matters here. -- **How to run it:** Teach the paint analogy first, *then* show the BIP352 flow. Don't open with math or you'll lose the room. -- **When you get stuck:** BIP352 is the spec; the Rust Silent Payments crates have readable examples. - -**Build:** Use a Silent Payments library to derive a payment address from a static one. -**Contribute:** Docs, examples, or tests in a Silent Payments crate. Pick from the pool. - ---- - -#### S2 · Scanning and the light-client connection -**Who it's for:** People who did S1. -**What you'll learn (plain):** Why finding your own Silent Payments is hard, and how compact block filters make it possible without a server spying on you. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** Because each Silent Payment lands at a fresh address, your wallet must scan blocks to recognize which are yours. Doing that privately (without telling a server your addresses) is where BIP157/158 filters come in - they let you check blocks yourself. -- **Analogy to use:** Instead of asking the post office "any mail for me?" (and revealing your name), you get a tiny summary of every mailbag and check it yourself at home. -- **Common questions:** - - *"Why not just ask a server?"* - Because then the server knows every address you own. The whole point is to not leak that. -- **How to run it:** Connect the dots between S1's scanning cost and the light-client sessions (N1). This is the bridge session. -- **When you get stuck:** BIP158 spec + Kyoto docs. - -**Build:** Demonstrate filtering a block for relevant outputs. -**Contribute:** A pool issue tying Silent Payments and filters together (often in Kyoto or a SP crate). - ---- - -### Network and node privacy - -#### N1 · Light clients and compact block filters (BIP157/158) -**Who it's for:** Anyone who uses a phone wallet. Moderate. -**What you'll learn (plain):** Why most "light" wallets leak your addresses to a server, and how compact block filters fix it. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** Old light wallets (BIP37) basically told a server which addresses to watch - handing over your identity. BIP157/158 flips it: the server sends tiny *summaries* of each block, and your wallet checks them privately at home. -- **Analogy to use:** BIP37 is telling the librarian exactly which books you want so they fetch them (now they know your interests). BIP158 is getting the catalog and finding your books yourself. -- **Common questions:** - - *"Is this slower?"* - There's more data to download, but you stop leaking your addresses. Privacy has a cost; this one's worth it. -- **How to run it:** Read Kyoto's README and find where filters are checked. Trace one block's path. -- **When you get stuck:** Kyoto docs + BIP158. - -**Build:** Run Kyoto against the network and watch it sync via filters. -**Contribute:** Docs/tests/examples in Kyoto. Pick from the pool. - ---- - -#### N2 · Run your own node privately (Floresta and Utreexo) -**Who it's for:** Anyone who wants to stop trusting someone else's node. 🟢 Welcoming. -**What you'll learn (plain):** Why running your own node is the single biggest privacy upgrade, and how Utreexo makes it light enough to be realistic. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** If you use someone else's node, they can see which addresses are yours and which transactions you broadcast. Your own node fixes that - but a normal full node is heavy. Utreexo shrinks the data you must store using a clever cryptographic accumulator, so a node can run on modest hardware. -- **Analogy to use:** Instead of keeping every receipt you've ever had (full node), you keep one cryptographic "fingerprint" that can still prove any receipt is real (Utreexo). -- **Common questions:** - - *"Do I lose security?"* - You still verify everything yourself; you just store a compressed proof structure instead of the whole set. -- **How to run it:** Build and run Floresta together. It's an active, friendly Rust project - great for first real contributions. -- **When you get stuck:** Floresta's docs and issue tracker are beginner-friendly. - -**Build:** Build and run a Floresta node. -**Contribute:** Floresta has a healthy good-first-issue flow - pick one from the pool (docs, tests, small fixes). - ---- - -#### N3 · P2P privacy, eclipse attacks, and node fingerprinting -**Who it's for:** The networking-curious. Moderate. -**What you'll learn (plain):** How merely *connecting* to the Bitcoin network can leak your IP or let an attacker surround your node. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** When your node broadcasts a transaction, the way it spreads can hint at which IP it started from. And if an attacker controls all the peers you connect to (an "eclipse"), they can feed you a fake view of the network or watch everything you do. -- **Analogy to use:** An eclipse attack is like someone quietly replacing everyone you talk to with their own actors - you think you're seeing the world, but you're only seeing what they show you. -- **Common questions:** - - *"How do I broadcast without leaking my IP?"* - Techniques like routing over Tor and careful peer selection. This session explains the threat; defenses are ongoing work. -- **How to run it:** Conceptual plus reading P2P/networking docs in Floresta or rust-bitcoin's networking pieces. -- **When you get stuck:** Stay conceptual; contributions are docs/tests. - -**Build:** Map your node's peer connections and discuss exposure. -**Contribute:** Documentation/test contribution on P2P behavior from the pool. - ---- - -#### N4 · ASmap - defending against network-level deanonymization -**Who it's for:** Those who did N3. Moderate. -**What you'll learn (plain):** How an attacker who controls chunks of internet infrastructure could deanonymize nodes, and how ASmap spreads your connections across independent networks to stop it. - -**Tutor preparation** *(prep: ~90 min)* -- **The concept in plain words:** The internet is divided into big networks ("autonomous systems"). If all your node's connections happen to run through one network an attacker controls, they can watch or isolate you. ASmap makes your node deliberately spread its connections across *different* networks so no single operator sees them all. -- **Analogy to use:** Don't send every messenger out the same gate - if one guard is bribed, they see everything. Send them through different gates. -- **Common questions:** - - *"Is this a real threat for normal users?"* - It's more relevant to dedicated attackers, but it raises everyone's baseline. Worth understanding. -- **How to run it:** Read how ASmap data is used in node connection logic. Conceptual + code-reading. -- **When you get stuck:** ASmap documentation and the relevant networking code. - -**Build:** Explain how ASmap would change your node's peer selection. -**Contribute:** Docs/test contribution related to ASmap or peer selection from the pool. - ---- +Full guide: [FACILITATOR_GUIDE.md](./FACILITATOR_GUIDE.md). ## The contribution ladder -You're never stuck at one level, and you can enter at any rung: +Enter at any rung. There's no pressure to climb. -1. **Engage** - build a repo, read its code, join its chat, understand an issue. -2. **Fix** - docs, comments, error messages, small clarity improvements. (Most first PRs.) -3. **Test** - add or improve test coverage. Maintainers love this and it teaches you the code. -4. **Build** - implement a small feature or fix a real bug from the pool. -5. **Ship** - take on a substantial issue and see it through review to merge. -6. **Lead** - curate issues for others, mentor newcomers, or become a regular contributor to a repo. (Several tutors started here.) +1. **Engage:** build a repo, read the code, join its chat +2. **Test:** build someone's PR, run it, report exactly what you did and saw +3. **Fix:** docs, error messages, small clarity improvements +4. **Cover:** add tests. Maintainers love it, and it teaches you the code +5. **Review:** explain a PR, challenge an assumption, find an edge case +6. **Build:** fix a real bug or implement a scoped feature, then see it through review +7. **Lead:** curate the pool, host a session, mentor newcomers -There's no pressure to climb. A program full of solid rung-2 and rung-3 contributions is a successful program. +## What we measure ---- +In the order that matters (details in the program's [MEASUREMENT.md](https://github.com/code-orange-dev/code-orange-dev/blob/main/privacy-contributor-program/MEASUREMENT.md)): -## What success looks like +1. **Retention:** are people still contributing to a privacy project 3, 6 and 12 months later? +2. **Useful upstream activity:** reviews, test reports, reproduced bugs, tests, docs and merged code, counted separately +3. **Maintainer sentiment:** do maintainers want our contributors back? This is the asset that compounds +4. **Return attendance:** drop-in is the model, and coming back is the signal it works -We measure four things, in order of importance: - -1. **Merged PRs** - the headline. Target: 50–100 merged per year across an active group of 10–20 contributors. -2. **Merge rate** - our quality proxy. If the share of PRs that get merged drops, our quality floor is too low and we tighten the checklist. A healthy merge rate is the real sign we're respecting maintainers. -3. **Repeat contributors** - are people coming back? Drop-in is the model, but people returning is the signal that it works. -4. **Maintainer sentiment** - the leading indicator almost nobody tracks. Are maintainers glad to see Code Orange contributors? This is the asset that compounds. Guard it. - ---- +## Files -## In short +| File | What it's for | +|---|---| +| [SESSIONS.md](./SESSIONS.md) | The 12 sessions: beats, prep, hands-on, PRs, follow-ups | +| [FACILITATOR_GUIDE.md](./FACILITATOR_GUIDE.md) | Running a session: checklist, Review Circle rules, troubleshooting | +| [ISSUE_POOL.md](./ISSUE_POOL.md) | Verified contribution targets, mapped to sessions | +| [PR_CHECKLIST.md](./PR_CHECKLIST.md) | Run before anything goes upstream | +| [resources/](./resources/) | Glossary and per-session reading list | +| [CONTRIBUTING.md](./CONTRIBUTING.md) | Improving this track | -Show up to any session. Learn one privacy topic properly. Pick a vetted issue, run the checklist, open a real pull request. Leave having made Bitcoin a little more private for everyone - whether it's your first PR or your fiftieth. +CC0: fork it, translate it, run it in your city. -Privacy isn't something you wait for. It's something you ship. +*Privacy isn't something you wait for. It's something you ship.* 🟠 diff --git a/privacy-track/SESSIONS.md b/privacy-track/SESSIONS.md index 9a14ad7..5dd882d 100644 --- a/privacy-track/SESSIONS.md +++ b/privacy-track/SESSIONS.md @@ -1,217 +1,226 @@ -# The Privacy Sessions - Biweekly Cypherpunk Hangouts +# The Privacy Sessions: Season One -**90 min. Every 2 weeks. Hands-on, discussion-driven, contribution-first.** +**90 min. Every 2 weeks. Hands-on, discussion-driven, review-first.** - A hangout for privacy-minded, cypherpunk Bitcoiners, not a lecture series -- Every session: use a real privacy tool, then review a real open PR together, live -- Drop in any session. No prerequisites, no sequence. Testnet always, cameras optional +- Every session: use or build a real privacy tool, then study a real open PR together +- Drop in any session. Each one re-teaches what it needs. Signet, testnet or regtest only, cameras optional -## Season One Schedule - -**Month 1** -- Session 1: Chain Analysis -- Session 2: Wallet Fingerprinting - -**Month 2** -- Session 3: Coin Selection & UTXO Management -- Session 4: Silent Payments - -**Month 3** -- Session 5: Payjoin -- Session 6: Light Clients (Floresta & Kyoto) +We open with Silent Payments because it's happening now. Bitcoin Core merged the BIP352 core logic in September 2026 ([#35301](https://github.com/bitcoin/bitcoin/pull/35301)), libsecp256k1 shipped a `silentpayments` module in v0.8.0, and sending ([#35302](https://github.com/bitcoin/bitcoin/pull/35302)) and receiving ([#32966](https://github.com/bitcoin/bitcoin/pull/32966)) are in review. Participants can learn the protocol, build it, and review it while the work is still going on. -**Month 4** -- Session 7: P2P Privacy & Node Fingerprinting -- Session 8: ASmap & Network Attacks - -**Month 5** -- Session 9: CoinJoin (JoinMarket) -- Session 10: OpenSwap (formerly CoinSwap) +## Season One Schedule -**Month 6** -- Session 11: Lightning Privacy -- Session 12: Contribution Sprint +| Month | Session | Topic | Lab / tool | +|---|---|---|---| +| 1 | S1 | Silent Payments I: One Address, No Trail | [SP lab](./labs/silent-payments/): build a sender | +| 1 | S2 | Silent Payments II: Finding Your Money | [SP lab](./labs/silent-payments/): build a scanner, indexers | +| 2 | S3 | Silent Payments III: Review Club | Bitcoin Core / libsecp256k1 SP PRs | +| 2 | S4 | Think Like the Adversary: Chain Analysis & Fingerprinting | [chain-analysis lab](./labs/chain-analysis/), real wallets | +| 3 | S5 | Coin Selection & Change | [coin-selection lab](./labs/coin-selection/), Sparrow, BDK | +| 3 | S6 | Payjoin | [payjoin lab](./labs/payjoin/): payjoin-cli on regtest | +| 4 | S7 | Light Clients & Your Own Node | [filters lab](./labs/compact-block-filters/), Kyoto, Floresta | +| 4 | S8 | The Network Sees You: Tor, BIP324, Private Broadcast, ASmap | [network lab](./labs/network-privacy/): Bitcoin Core 31 | +| 5 | S9 | Breaking the Trail: CoinJoin & OpenSwap | JoinMarket, openswap regtest | +| 5 | S10 | Lightning Privacy | BOLT11 vs BOLT12 on regtest | +| 6 | S11 | Ecash: Cashu & Fedimint | a signet mint | +| 6 | S12 | Contribution Sprint | the season's issue board | --- -## The Format - same 6 beats, every session +## The Format: the same 6 beats every session -| Beat | Time | What happens | +| Beat | Min | What happens | |---|---|---| -| 🩸 Cold Open | 10 | True surveillance story. Sets stakes, starts the argument | -| 🕳 Rabbit Hole | 15 | The concept, plain language, one analogy | +| 🩸 Cold Open | 10 | A true surveillance story. Sets the stakes and starts the argument | +| 🕳 Rabbit Hole | 15 | The concept in plain language, with one analogy | | 🛠 Hands-On | 30 | Everyone does it live on their own machine | -| 🔍 Review Circle | 20 | Read a live PR together. Someone posts a real review comment before it ends | -| 📜 Cypherpunk Corner | 10 | Read a short primary text aloud. Argue | -| 🎯 Bounty Board | 5 | Claim homework. Sats for completions | +| 🔍 Review Circle | 20 | Read a live PR together: what does it change, how would we test it, what would we ask? | +| 📜 Cypherpunk Corner | 10 | Read a short primary text aloud, then argue about it | +| 🎯 Proof-of-Work Board | 5 | Claim one concrete follow-up: a lab finished, a PR tested, an issue reproduced, a doc fixed | **Facilitator basics (every session):** -- You're a host, not a professor. Don't know? Say so, read the code together -- Prep takes ~30 min: pick 2-3 small live PRs for the Review Circle + 3-5 bounty items, verify open that morning -- Track one metric: reviews + PRs posted by the room. Post to the [PR dashboard](https://github.com/code-orange-dev/PR-tracking-dashboard) -- Testnet/signet only. No real balances on screen. Pseudonyms welcome +- You're a host, not a professor. If you don't know something, say so and read the code together +- Prep takes ~30-60 min: do the hands-on yourself, pick 1-2 live PRs for the Review Circle, and check that the issues on the board are still open *that morning* +- **The Review Circle does not post upstream on a timer.** Comments go upstream only when they're genuinely useful, e.g. a test report with steps, a reproduced bug, or a question the thread hasn't answered. A clean "built and tested on macOS, here's what I ran" is valuable. Filler ("nice work!", "concept ACK" without reasoning) costs maintainers time and costs us trust. See [PR_CHECKLIST.md](./PR_CHECKLIST.md) +- Sats reward completed proof of work (a finished lab, a test report, a merged doc fix), never comment counts +- Record evidence per the [contributor program](https://github.com/code-orange-dev/code-orange-dev/tree/main/privacy-contributor-program): what was reviewed/built/tested, and links. Merged PRs go on the [PR dashboard](https://github.com/code-orange-dev/PR-tracking-dashboard) +- Test networks only. No real balances on screen. Pseudonyms welcome --- -## S1 · Chain Analysis - "You Are Being Watched" - -**You leave with:** the 5 surveillance heuristics + your first diff read. -**Prep:** make 3 testnet txs beforehand (for the game) · pick a real deanonymization case on mempool.space · pick 2 PRs ([mempool](https://github.com/mempool/mempool) or [BDK](https://github.com/bitcoindevkit/bdk_wallet)) · print Hughes quote - -- 🩸 Walk a real deanonymization live: one address reuse, whole life exposed -- 🕳 The 5 heuristics: common-input-ownership, address reuse, round amounts, change detection, wallet fingerprinting. Analogy: binoculars at a market -- 🛠 **HUNT EACH OTHER:** room plays analyst on your 3 planted txs. Find change, guess wallet, trace money. Sats for first correct trace -- 🔍 Learn to read a diff. One brave soul comments -- 📜 Hughes, *A Cypherpunk's Manifesto* (1993): "Privacy is the power to selectively reveal oneself" -- 🎯 Run the 5 heuristics on your own old tx. Share what you leaked (voluntarily) - -## S2 · Wallet Fingerprinting - "Your Wallet Has Handwriting" - -**You leave with:** ability to identify a wallet from raw hex. -**Prep:** install 3-4 wallets (Sparrow, Electrum, Cake, bdk-cli) · start a shared fingerprint-table doc · pick 2 [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin) tx-construction PRs - -- 🩸 Two identical-looking txs on screen. By night's end the room tells them apart. So does Chainalysis -- 🕳 The tells: nLockTime, RBF flags, input ordering (BIP69 or not), version bytes -- 🛠 **BUILD THE FINGERPRINT TABLE:** everyone makes a testnet tx with a different wallet, dump hex, compare, fill the table. It grows every season -- 🔍 rust-bitcoin PR: "does this change what a tx looks like on-chain?" -- 📜 Whitepaper section 10: the original privacy model and where it broke -- 🎯 Add a wallet row to the table, or file a fingerprint docs issue upstream - -## S3 · Coin Selection & UTXO Management - "Coins Have Memories" - -**You leave with:** labeled UTXOs + fear of careless merging. -**Prep:** Sparrow on testnet with a funded wallet · the frozen-funds story · 2 [BDK](https://github.com/bitcoindevkit/bdk_wallet) coin-selection PRs · one merged Code Orange BDK PR to show - -- 🩸 Exchange freezes a user for where coins were 3 hops ago -- 🕳 UTXOs = coins with memories. Coin selection = choosing which past life to reveal. Labels are self-defense -- 🛠 **BREAK IT ON PURPOSE:** label every UTXO, freeze one, force selections. Then merge two labeled identities and watch the damage -- 🔍 BDK PRs. Show Muhammad's merged audit fixes: normal people do this +## S1 · Silent Payments I: "One Address, No Trail" + +**You leave with:** your own BIP352 sender passing the official test vectors, and a failed attempt to surveil an SP address. +**Prep:** run `python3 run_vectors.py send --solution` in [labs/silent-payments](./labs/silent-payments/) · pick a public static donation address (a podcaster or open-source project) and pre-load its history on mempool.space · have Dana wallet (signet) or another SP wallet ready to show a real `tsp1...` address · skim [BIP352](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) "Overview" and "Creating outputs" + +- 🩸 Scrape the static donation address live: every donation, every amount, when they cash out. That's what reusing an address does. Then show an SP address that has received many payments: on-chain there's nothing to link +- 🕳 The problem SP solves: you want to publish one address, but addresses shouldn't be reused. BIP47 needed a notification transaction, and xpub-sharing needed a server. SP needs neither. + - **Analogy (paint mixing):** the sender mixes their secret color (input keys) with your public color (scan key). Only you can remake that mix from your side, so only you recognize the result + - Walk through: `B_scan` / `B_spend`, sum of input keys, input hash (why paying the same person twice gives two different addresses), `t_k`, the x-only taproot output + - The catch, which is S2's topic: the receiver has to *scan* +- 🛠 **BUILD THE SENDER** ([send.py](./labs/silent-payments/send.py)): 5 small functions, graded against the 28 official vectors. Pair people up, and let the room race to 28/28. Anyone who finishes early reads `txin.py` and explains to the room why P2WSH multisig inputs don't count +- 🔍 [bitcoin/bitcoin#36338](https://github.com/bitcoin/bitcoin/pull/36338) *BIP-352: fix P2PKH pubkey extraction from malleated scriptSig*. It's small, it's live, and it maps directly onto lab vector 21. Questions for the room: what did the old code do, and what would happen to a payment if sender and receiver disagreed here? +- 📜 BIP352 "Motivation". Read it like scripture, argue about it like heretics: does SP make the reuse problem go away, or just move it somewhere else? +- 🎯 Finish the lab to 28/28 · or test Dana/Shroud on signet and file one well-documented bug or UX issue · or read [bdk-sp#55](https://github.com/bitcoindevkit/bdk-sp/issues/55) and check whether vector 27 covers it + +## S2 · Silent Payments II: "Finding Your Money" + +**You leave with:** a working scanner, a number for "how long to scan a day of mainnet", and an opinion on who should do that work. +**Prep:** `python3 run_vectors.py receive --solution` and `python3 scan_benchmark.py` · read the [BIP352 index server spec](https://github.com/silent-payments/BIP0352-index-server-specification) · know the three scanning models below well enough to draw them · optional: a signet BlindBit Oracle or Frigate instance to demo + +- 🩸 "Your phone has to do *what*?" Receiving SP means checking every eligible transaction on the chain. Run the benchmark live, then scale it: a year offline equals X hours of catch-up +- 🕳 Three ways to scan, each with its own privacy trade-off: + 1. **Your own full node** (Bitcoin Core receiving PR, kernel-node): private, but heavy + 2. **Download tweaks** from an indexer (BlindBit Oracle, Shroud's indexer, Dana): the server only learns that you're an SP user, and you do the ECDH yourself. Pair this with BIP158 filters so you don't reveal which outputs matched (bridge to S7) + 3. **Hand your scan key to a server** (Frigate, with ephemeral keys held in RAM): fast and GPU-accelerated, but the server can see your incoming payments + - **Analogy:** (1) you sort all the mail at the post office yourself; (2) the post office gives you a stamp for each letter and you test them at home; (3) you give the postman a key that opens only your letters, which saves time, and he can read them + - Labels: one wallet, many distinguishable addresses, still one scan +- 🛠 **BUILD THE SCANNER** ([receive.py](./labs/silent-payments/receive.py)): tweak, scan loop, then labels and spend keys as stretch goals. The grader catches the two classic bugs (checking outputs by position, stopping after the first match). Finish with `scan_benchmark.py` and compare laptops +- 🔍 [bitcoin-core/secp256k1#1912](https://github.com/bitcoin-core/secp256k1/pull/1912) *silentpayments: add light client API*. Which of our three models is this API for? Or, from our own community: [Shroud](https://github.com/CypherCommons/shroud) (Chaitika's wallet) and [kernel-node#50](https://github.com/kernel-node/kernel-node/pull/50) (Peter's SP scanning, merged) +- 📜 Satoshi, whitepaper §10 "Privacy": "keeping public keys anonymous." How far did we drift, and does SP bring us back? +- 🎯 [shroud#132](https://github.com/CypherCommons/shroud/issues/132): unit tests for its Rust BIP-352 scanner (you just learned what to test) · or run BlindBit Oracle on signet and write up the setup · or take on [danawallet#466](https://github.com/cygnet3/danawallet/issues/466) (the output-ambiguity research issue) if you finished the labels stretch + +## S3 · Silent Payments III: Review Club, "Silent Payments Land in Core" + +**You leave with:** a Bitcoin Core branch built, the BIP352 unit tests run, and a written review of real SP code. +**Prep:** this is a [Review Club](https://github.com/code-orange-dev/code-orange-dev/blob/main/privacy-contributor-program/REVIEW_CLUB.md) session, so announce the PR **one week ahead** and ask people to build it beforehand. Candidates, in order of approachability: [#35301](https://github.com/bitcoin/bitcoin/pull/35301) (merged BIP352 core logic, good for learning the layout), [#35302](https://github.com/bitcoin/bitcoin/pull/35302) (sending, open), [#32966](https://github.com/bitcoin/bitcoin/pull/32966) (receiving, open). Read the [tracking issue #28536](https://github.com/bitcoin/bitcoin/issues/28536). Have a pre-built node ready for people whose builds failed + +This session replaces the usual beats with the Review Club format: +- **Context (10):** why SP in Core took three years, and the "take 2" story: first-generation PRs were rebuilt on the libsecp256k1 module ([#1765](https://github.com/bitcoin-core/secp256k1/pull/1765)). What does that ordering tell you about how Core de-risks cryptography? +- **Participants explain (15):** someone who did S1 maps `send.py` to `src/common/bip352.cpp`. Where do `a_sum`, `input_hash` and `t_k` live? Spot the same test-vector file the lab uses in `src/test/data/` +- **Test evidence (15):** compare build/test results across machines. Did everyone's `test_bitcoin --run_test=bip352_tests` pass? On the open PRs, which tests did they add? +- **Conceptual review (25):** what's in scope and what's deferred (labels, wallet integration)? What would you want tested before this reaches users? Where could sender and receiver disagree? +- **Code/test review (15):** pick one function per pair and read it line by line +- **Upstream action (10):** decide together whether anything is worth posting. A tested-on-platform report with exact steps usually is. If nothing is, that's fine. Record the evidence either way +- 🎯 Keep a running review on the open SP PR through the season; carry your notes to S12 + +## S4 · Think Like the Adversary: "You Are Being Watched" + +**You leave with:** the 5 chain-analysis heuristics, a threat model you can reuse, and the ability to name a wallet from raw hex. +**Prep:** make 3 signet transactions from different wallets beforehand (Sparrow, Electrum, Bitcoin Core, bdk-cli) · pick a real deanonymization case on mempool.space · start the shared fingerprint-table doc · 1-2 [BDK](https://github.com/bitcoindevkit/bdk_wallet) or [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin) tx-construction PRs + +- 🩸 Walk through a real deanonymization live: one address reuse exposes a whole life. Then put two near-identical transactions on screen. By the end of the night the room can tell them apart, and so can Chainalysis +- 🕳 The reusable framework: **Adversary → Observations → Heuristics → Leakage → Mitigation → Trade-offs**. Heuristics: common-input-ownership, address reuse, round amounts, change detection, wallet fingerprints (nLockTime/anti-fee-sniping, nSequence/RBF, input/output ordering, version, script types). **Analogy:** handwriting analysis on envelopes +- 🛠 **HUNT EACH OTHER:** the room plays analyst on your 3 planted txs: find the change, guess the wallet, trace the money. Then everyone dumps their own wallet's tx hex into the shared fingerprint table. The table grows every season. Solo warm-up: [chain-analysis lab](./labs/chain-analysis/) +- 🔍 A tx-construction PR: "does this change what a transaction looks like on-chain?" +- 📜 Hughes, *A Cypherpunk's Manifesto* (1993): "Privacy is the power to selectively reveal oneself to the world" +- 🎯 Add a wallet row to the fingerprint table with evidence · or run the heuristics on one of your own old txs and write up what leaked (only if you want to share) + +## S5 · Coin Selection & Change: "Coins Have Memories" + +**You leave with:** labeled UTXOs and a healthy fear of careless merging. +**Prep:** Sparrow on signet with a funded wallet · the frozen-funds story · 2 [BDK](https://github.com/bitcoindevkit/bdk_wallet) coin-selection PRs · one merged Code Orange BDK PR to show + +- 🩸 An exchange freezes a user because of where their coins were 3 hops ago +- 🕳 UTXOs are coins with memories, and coin selection chooses which past life to reveal. Algorithms (largest-first, BnB, random) trade fees against privacy. Changeless transactions, labels as self-defense, BIP329 label export +- 🛠 **BREAK IT ON PURPOSE:** label every UTXO, freeze one, force selections. Then merge two labeled identities and watch the damage. Then run the [coin-selection lab](./labs/coin-selection/) and try to beat BnB on privacy without losing on fees +- 🔍 BDK coin-selection PRs. Show [Muhammad's merged audit fixes](https://github.com/bitcoindevkit/bdk_wallet/pull/471): normal people do this - 📜 Hal Finney on privacy expectations -- 🎯 BDK docs/test issue, or async review of one open PR - -## S4 · Silent Payments - "One Address to Rule Them All" - -**You leave with:** an SP payment sent + a failed trace. -**Prep:** SP-capable wallet ready (Cake or tooling) · a public donation address to scrape live · 2 PRs from [shroud](https://github.com/CypherCommons/shroud) or [rust-silentpayments](https://github.com/cygnet3/rust-silentpayments) - -- 🩸 Scrape a podcaster's static donation address: whole financial history. Then an SP address: nothing -- 🕳 BIP352, paint-mixing analogy: sender's secret color + your public color = address only you detect. Catch: receiver must scan -- 🛠 **FAIL TO SURVEIL:** pay each other via SP on testnet, then try to link payments on-chain. The failure is the demo -- 🔍 shroud is our own community's wallet (Chaitika). Author may be in the call -- 📜 BIP352 motivation section. Read like scripture, argue like heretics -- 🎯 Test shroud and file a bug/UX issue, or review an SP PR - -## S5 · Payjoin - "Paying Together" - -**You leave with:** a payjoin done in pairs + a poisoned heuristic. -**Prep:** payjoin-cli working on signet (test it yourself first!) · 1 pre-picked [rust-payjoin](https://github.com/payjoin/rust-payjoin) PR for swarm review · good-first-issue list open - -- 🩸 The analyst's crown jewel (all inputs = one owner) and the tx type that poisons it for everyone -- 🕳 BIP78 sync, BIP77 async. Analogy: both people put cash on the restaurant table -- 🛠 **PAYJOIN IN PAIRS:** one sends, one receives, swap roles. Inspect the tx: point at the lie it tells -- 🔍 Friendliest repo we know: 4 Code Orange contributors merged (Arowolo, Vaan, Mwihoti). Swarm-review one PR -- 📜 Adam Back: privacy must be default and boring to win -- 🎯 Everyone claims one rust-payjoin good-first-issue. Docs count +- 🎯 A BDK beginner issue from the [pool](./ISSUE_POOL.md) · or a test report on an open coin-selection PR -## S6 · Light Clients (Floresta & Kyoto) - "Don't Trust, Verify - Privately" +## S6 · Payjoin: "Paying Together" -**You leave with:** your own node syncing, wallet pointed at it. -**Prep:** build [Floresta](https://github.com/vinteumorg/Floresta) yourself first, note the gotchas · sats for the race · 2 Floresta/[Kyoto](https://github.com/rustaceanrob/kyoto) PRs +**You leave with:** a payjoin done in pairs and a broken heuristic. +**Prep:** run the [payjoin lab](./labs/payjoin/) end to end yourself (nigiri + `payjoin-cli` on regtest) · 1 pre-picked [rust-payjoin](https://github.com/payjoin/rust-payjoin) PR for swarm review · beginner-issue list open -- 🩸 What your light wallet tells the server: every address, IP, balance, timing -- 🕳 Someone else's node = stranger knows your net worth. Filters (BIP157/158) + Utreexo. Analogy: fetch the catalog vs telling the librarian everything -- 🛠 **NODE RACE:** build and run Floresta live. First sync wins sats. Connect a wallet to YOUR node -- 🔍 Floresta: welcoming maintainers, real good-first-issue culture -- 📜 "Don't trust, verify": what it actually demands of us -- 🎯 Floresta good-first-issue, or turn tonight's setup pain into a docs PR +- 🩸 The analyst's crown jewel (all inputs belong to one owner) and the transaction type that poisons it for everyone, including people who never use it +- 🕳 BIP78 (sync, the receiver runs a server) vs BIP77 (async, via an untrusted directory with OHTTP). **Analogy:** both people put cash on the restaurant table. Where it ships today: [payjoin.org](https://payjoin.org) lists wallets +- 🛠 **PAYJOIN IN PAIRS** ([payjoin lab](./labs/payjoin/)): one person sends, one receives, then swap roles. Then play analyst: trace each input back to its wallet and point at the lie the transaction tells +- 🔍 The friendliest repo we know: 4 Code Orange contributors have merged here (Arowolo, Vaan, Mwihoti). Swarm-review one PR +- 📜 Adam Back: privacy has to be default and boring to win +- 🎯 A rust-payjoin beginner issue from the [pool](./ISSUE_POOL.md). Docs count ([#865 `cargo doc` improvements](https://github.com/payjoin/rust-payjoin/issues/865)) -## S7 · P2P Privacy & Node Fingerprinting - "The Network Sees You" +## S7 · Light Clients & Your Own Node: "Don't Trust, Verify, Privately" -**You leave with:** your node traffic inspected, then Tor-routed. -**Prep:** Wireshark installed + a node you can capture · Tor configured · 2 [peer-observer](https://github.com/peer-observer/peer-observer) PRs · May manifesto quote +**You leave with:** your own node syncing, and your wallet pointed at it. +**Prep:** build [Floresta](https://github.com/getfloresta/Floresta) yourself first and note the gotchas · run the [filters lab](./labs/compact-block-filters/) · sats for the race · 2 Floresta/[Kyoto](https://github.com/2140-dev/kyoto) PRs -- 🩸 Research tracing txs to originating IPs via listener networks. ISP identity glued to your coins -- 🕳 How txs propagate, who listens, Dandelion++, Tor, BIP324. Broadcast privacy is unsolved -- 🛠 **WIRETAP YOURSELF:** Wireshark your node plaintext vs BIP324. Then route over Tor, compare what an observer sees -- 🔍 peer-observer: our own Razor has 4 merged PRs here. Review with "what does this detect?" glasses -- 📜 May, *Crypto Anarchist Manifesto* (1988) -- 🎯 peer-observer issue, or write up your Tor setup as a guide +- 🩸 What your light wallet tells the server: every address, your IP, your balance, your timing +- 🕳 Using someone else's node means a stranger knows your net worth. BIP37 bloom filters leaked; BIP157/158 compact filters flip the model; Utreexo makes a full node light. **Analogy:** telling the librarian every book you want vs taking the catalog home. Tie-in to S2: filters are how an SP light client fetches only the blocks it needs without saying which +- 🛠 **NODE RACE:** build and run Floresta live, and the first to sync wins sats. Connect a wallet to YOUR node. Faster finishers: the filters lab +- 🔍 Floresta: welcoming maintainers and a real good-first-issue culture +- 📜 "Don't trust, verify": what it actually asks of us +- 🎯 A Floresta beginner issue ([#799 RPC docs](https://github.com/getfloresta/Floresta/issues/799) is ideal after tonight) · or turn tonight's setup pain into a docs PR -## S8 · ASmap & Network Attacks - "Surrounded" +## S8 · The Network Sees You: "Tor, BIP324, Private Broadcast, ASmap" -**You leave with:** a map of your node's network exposure + a diversification plan. -**Prep:** a running Bitcoin Core node with peers · AS-lookup tool ready · asmap docs · 1-2 asmap/peer-selection PRs or peer-observer detection work +**You leave with:** your node's traffic inspected, your broadcasts made private, and a map of your network exposure. +**Prep:** run the [network lab](./labs/network-privacy/) end to end on signet (Bitcoin Core **31.x** + Tor + Wireshark) · read the 31.0 release notes on `-privatebroadcast` and embedded asmap · 2 [peer-observer](https://github.com/peer-observer/peer-observer) PRs · the May quote -- 🩸 The eclipse attack as a heist: replace everyone a node talks to, show it a fake world -- 🕳 The internet = autonomous systems. One operator on all your connections can watch or isolate you. ASmap spreads peers across networks. Analogy: never send all messengers out the same gate -- 🛠 **MAP YOUR EXPOSURE:** pull your peer list, look up each AS, count peers sharing one network. Enable asmap, compare diversity before/after -- 🔍 Core asmap/peer-selection PRs, or peer-observer -- 📜 Debate: "the network layer is Bitcoin's soft underbelly." Overblown or underrated? -- 🎯 Publish your AS-diversity check as a guide, or review an asmap PR +- 🩸 Research tracing transactions to originating IPs through listener networks: your ISP identity glued to your coins. Then the eclipse attack as a heist: replace everyone a node talks to and show it a fake world +- 🕳 How transactions propagate, who's listening, and the three layers of defense that shipped: + - **BIP324** encrypted transport (hides content from your ISP, not from peers) + - **`-privatebroadcast`** (Core 31): transactions submitted with `sendrawtransaction` go out only via short-lived Tor/I2P connections, one tx per connection. Wallet sends aren't covered yet; open follow-ups like [#34533](https://github.com/bitcoin/bitcoin/pull/34533) work on that + - **`-asmap=1`** (Core 31, embedded map, off by default): spread your peers across networks so no single operator surrounds you. **Analogy:** never send all your messengers out the same gate +- 🛠 **WIRETAP YOURSELF** ([network lab](./labs/network-privacy/)): Wireshark plaintext v1 vs BIP324. Enable `-privatebroadcast`, build a signet tx in the wallet and broadcast it with `sendrawtransaction` (in Core 31, wallet sends don't use private broadcast), then watch `getprivatebroadcastinfo`. Count peers sharing one AS, enable `-asmap=1`, compare diversity +- 🔍 peer-observer: our own Razor has 4 merged PRs here, so review with "what does this detect?" glasses. Or an open private-broadcast follow-up in Core (e.g. [#34322](https://github.com/bitcoin/bitcoin/pull/34322), [#34533](https://github.com/bitcoin/bitcoin/pull/34533)) +- 📜 May, *The Crypto Anarchist Manifesto* (1988). Debate: "the network layer is Bitcoin's soft underbelly." Overblown or underrated? +- 🎯 A peer-observer beginner issue · or publish your AS-diversity check as a guide · or a tested-on-platform report on a private-broadcast PR -## S9 · CoinJoin (JoinMarket) - "Mixing Without Trust" +## S9 · Breaking the Trail: "CoinJoin & OpenSwap" -**You leave with:** a dissected real CoinJoin + a post-mix hygiene checklist. -**Prep:** pick a historical CoinJoin tx to autopsy · Samourai case summary · [JoinMarket](https://github.com/JoinMarket-Org) PRs · debate prompts +**You leave with:** a dissected real CoinJoin, a post-mix hygiene checklist, and an atomic swap on regtest. +**Prep:** pick a historical CoinJoin tx to autopsy · Samourai case summary · run the [openswap](https://github.com/citadel-foss/openswap) regtest framework yourself (makerd + taker) · Belcher's CoinSwap design post -- 🩸 The Samourai prosecution: what exactly is being fought over -- 🕳 Identical-envelopes-in-a-box analogy. Equal outputs, coordinator risk, JoinMarket's maker/taker market as the trustless answer -- 🛠 **AUTOPSY A COINJOIN:** count the anonymity set, then find the post-mix mistakes that unmixed people's coins. Privacy is a practice, not a purchase -- 🔍 JoinMarket ecosystem PRs, or coinjoin-detection code. Know thy enemy -- 📜 DEBATE: "Using a mixer: moral act, neutral act, or red flag?" Steelman all three -- 🎯 Write the post-mix hygiene checklist as a repo doc, or review a PR +- 🩸 The Samourai prosecution: what exactly is being fought over? Then: following the money works because money has one trail. Tonight we look at two ways to cut it +- 🕳 **CoinJoin:** identical envelopes in a box. Equal outputs, anonymity sets, coordinator risk, JoinMarket's maker/taker market as the trustless answer. **Swaps:** trade coin histories with a stranger, like swapping gift cards. On-chain it looks like two boring unrelated payments, and it doesn't look like mixing at all. Fidelity bonds stop Sybils (both JoinMarket and OpenSwap use them) +- 🛠 **AUTOPSY + SWAP:** (15 min) count a real CoinJoin's anonymity set, then find the post-mix mistakes that un-mixed people's coins. (15 min) pairs run makerd + taker on regtest and complete a swap. Challenge: prove a swap happened at all +- 🔍 [JoinMarket](https://github.com/JoinMarket-Org/joinmarket-clientserver) (a deep good-first-issue list) or openswap (a young repo where reviews genuinely matter) +- 📜 Debate: "Using a mixer: moral act, neutral act, or red flag?" Steelman all three +- 🎯 A JoinMarket beginner issue · or write tonight's openswap walkthrough as docs · or the post-mix hygiene checklist -## S10 · OpenSwap - "The Swap That Leaves No Trace" +## S10 · Lightning Privacy: "Lightning Doesn't Fix This" -**You leave with:** an atomic swap on regtest + a coin whose history isn't yours. -**Prep:** run the [openswap](https://github.com/citadel-foss/openswap) regtest framework yourself first (makerd + taker) · Belcher's CoinSwap design post · good-first-issue list +**You leave with:** a probed channel, a traced hop, and a blinded path that beat both. +**Prep:** regtest LN setup with 3+ nodes (Polar is fastest) · a BOLT11 invoice and a BOLT12 offer ready · [LDK](https://github.com/lightningdevkit/rust-lightning)/[ldk-node](https://github.com/lightningdevkit/ldk-node)/Core Lightning offers PRs -- 🩸 Following the money works because money has one trail. Tonight we cut it - on-chain it's two boring unrelated payments -- 🕳 OpenSwap (formerly CoinSwap): trustless atomic swaps. Gift-card-swap analogy: trackers now follow a card that was never yours. vs CoinJoin: swaps don't even look like mixing. Makers earn fees, takers pay, fidelity bonds stop Sybils, Tor multi-hop hides the route. Extends Belcher's teleport-transactions, now Taproot+MuSig2 -- 🛠 **SWAP WITH A STRANGER:** pairs run makerd + taker on regtest, complete a swap, inspect both tx chains. Challenge: prove a swap happened at all. Struggle -- 🔍 openswap: young repo, active good-first-issue label, reviews genuinely matter -- 📜 Belcher's original CoinSwap post: the vision, then how far the code came -- 🎯 openswap good-first-issue, or write tonight's walkthrough as docs +- 🩸 "Just use Lightning for privacy." Then show balance probing and a traced payment +- 🕳 What it hides: amounts from the chain. What it leaks: channels are public UTXOs, balances can be probed, invoices link identities, and your node pubkey is a fingerprint. Fixes in progress: BOLT12 offers, blinded paths, unannounced channels, splicing +- 🛠 **LEAK COMPARISON:** pay a BOLT11 invoice and list what each hop learned. Then pay a BOLT12 offer with a blinded path and compare line by line +- 🔍 ldk-node BOLT12 interop tests or Core Lightning offers (Gradale's territory) +- 📜 Debate: "Layer 2 inherits layer 1's sins" +- 🎯 An LDK/CLN docs or test issue on offers/blinded paths -## S11 · Lightning Privacy - "Lightning Doesn't Fix This" +## S11 · Ecash: "Blind Signatures, Community Custody" -**You leave with:** a probed channel, a traced hop, a blinded path that beat both. -**Prep:** regtest/signet LN setup with 3+ nodes · BOLT11 invoice + BOLT12 offer ready · [LDK](https://github.com/lightningdevkit/rust-lightning)/CLN offers PRs +**You leave with:** ecash minted, sent and redeemed on a test mint, plus a clear picture of what you traded away for that privacy. +**Prep:** a signet/testnut Cashu mint (or a local `cdk-mintd`) and a Fedimint test federation (Code Orange's Bali federation experience helps here) · wallets installed · 1-2 [cdk](https://github.com/cashubtc/cdk) or [Fedimint](https://github.com/fedimint/fedimint) PRs -- 🩸 "Just use Lightning for privacy." Then show balance probing + a traced payment -- 🕳 Hides: amounts from chain. Leaks: channels are public UTXOs, balances probeable, invoices link identity. Fix in progress: BOLT12 + blinded paths -- 🛠 **LEAK COMPARISON:** pay a BOLT11 invoice, list what each hop learned. Then BOLT12 with blinded path, compare line by line -- 🔍 LDK/ldk-node BOLT12 PRs (Gradale's territory) or Core Lightning offers -- 📜 Debate: "Layer 2 inherits layer 1's sins" -- 🎯 LDK/CLN docs or test issue on offers/blinded paths +- 🩸 The mint knows it issued 1,000 sats and later redeemed 1,000 sats. It can't tell they were the same person, and that's the whole trick +- 🕳 Chaumian blind signatures in 5 minutes. **Analogy:** signing a sealed envelope through carbon paper. Cashu (single mint) vs Fedimint (federated guardians). The trade: you get excellent privacy from the mint, and you give up self-custody. Who is this for? Remittances, community savings, onboarding +- 🛠 **MINT, SEND, REDEEM:** mint tokens, send them over a chat message, redeem them. Then play the mint operator: what do your logs actually show? +- 🔍 A cdk or Fedimint PR touching privacy or the wallet +- 📜 Chaum, *Security without Identification* (1985), the paper that started it +- 🎯 A cdk/Fedimint docs or test issue · or a local-language guide to ecash for your community -## S12 · Contribution Sprint - "Ship It" +## S12 · Contribution Sprint: "Ship It" -**You leave with:** something real submitted upstream, tonight. -**Prep:** the season scoreboard (names + numbers on screen) · curated board of verified-open issues across all repos · sats budget · Hughes closing quote +**You leave with:** something real submitted upstream tonight, whether that's code, a test report or a review that passed the checklist. +**Prep:** the season scoreboard (reviews, test reports, issues and PRs, with names on screen if people opted in) · a curated board of verified-open issues from the [pool](./ISSUE_POOL.md) · sats budget · the Hughes closing quote -- 🩸 The scoreboard: every review, issue, PR the room shipped. Names on screen -- 🛠 **THE SPRINT (60 min):** pick from the board (rust-payjoin, shroud, Floresta, Kyoto, BDK, openswap, peer-observer), pair up, ship live. Facilitators float, sats flow -- 🔍 Review each other's drafts before they go upstream. Quality floor in action -- 📜 Hughes' closing lines, read AFTER the sprint: "Cypherpunks write code... we're going to write it" -- 🎯 Vote Season Two topics +- 🩸 The scoreboard: everything the room shipped this season, including the reviews and test reports, not just PRs +- 🛠 **THE SPRINT (60 min):** pick from the board (SP scanner tests for Shroud, rust-payjoin, Floresta, Kyoto, BDK, peer-observer, JoinMarket, openswap, cdk), pair up, and ship it live. Facilitators float and sats flow +- 🔍 Everyone reviews someone else's draft against the [PR checklist](./PR_CHECKLIST.md) before it goes upstream +- 📜 Hughes' closing lines, read AFTER the sprint: "Cypherpunks write code" +- 🎯 Vote on Season Two topics. Strong contributors: talk to us about the [Review Club and fellowship](https://github.com/code-orange-dev/fellowships) --- -## Season Two candidates (room votes at S12) +## Season Two candidates (the room votes at S12) -- Ecash: Fedimint and Cashu (blind signatures, our Bali federation) -- The Exit: BTCPay Server and merchant privacy -- BIP324 encrypted transport deep-dive -- Mempool privacy and RBF +- BIP324 deep-dive: build a v2 transport handshake (Razor's [bip324-mitm](https://github.com/RazorBest/bip324-mitm) as a reference) +- BIP375/BIP374: Silent Payments with PSBTs and hardware wallets (DLEQ proofs) +- Stratum V2 and miner privacy +- Mempool privacy, RBF and fee fingerprinting - Nostr key hygiene for Bitcoiners +- Taproot, MuSig2 and FROST: multisig that looks like singlesig - Build our own privacy-scoring tool (original code!) -- Stratum V2 and miner privacy +- The Exit: BTCPay Server and merchant privacy --- ## Why these repos -rust-payjoin (4 CO contributors merged) · shroud (Chaitika) · peer-observer (Razor, 4 merged) · BDK (Vaan, Muhammad) · Floresta · Kyoto · openswap · LDK (Gradale, Psychemist). Newcomers see our own people's merged code in every repo. Anything upstream passes the [PR checklist](./PR_CHECKLIST.md). +Newcomers see our own people's merged code in almost every repo. rust-payjoin (4 CO contributors merged) · Shroud (Chaitika) · kernel-node and Bitcoin Core SP (Peter) · peer-observer (Razor, 4 merged) · BDK (Vaan, Muhammad) · LDK and Core Lightning (Gradale, Psychemist) · Floresta · Kyoto · openswap · JoinMarket · cdk. Anything that goes upstream passes the [PR checklist](./PR_CHECKLIST.md). Live targets are in the [issue pool](./ISSUE_POOL.md). *Privacy isn't something you wait for. It's something you ship.* 🟠 diff --git a/privacy-track/capstone/README.md b/privacy-track/capstone/README.md deleted file mode 100644 index ac7291b..0000000 --- a/privacy-track/capstone/README.md +++ /dev/null @@ -1,207 +0,0 @@ -# Capstone Project - Bitcoin Privacy Developer Track - -> Design, implement, and document a meaningful contribution to Bitcoin's privacy infrastructure. - ---- - -## Overview - -The capstone is the culmination of the privacy track. You will choose a project that combines what you've learned across all four modules and produce something that matters - code that gets merged, analysis that informs development, or a tool that helps other developers. - -This is not a homework assignment. It's a portfolio piece that demonstrates you can contribute to Bitcoin's privacy infrastructure at a professional level. - ---- - -## Timeline - -| Week | Milestone | -|------|-----------| -| Week 1 | Submit proposal (1 page) for facilitator approval | -| Week 2-3 | Implementation and iteration | -| Week 4 | Submit deliverables + present to cohort | - ---- - -## Project Options - -Choose ONE of the following tracks, or propose your own (must be approved by facilitator). - -### Track A: Code Contribution - -**Goal:** Ship a meaningful PR to a privacy-related Bitcoin project. - -**Requirements:** -- Submit a PR to one of the target repos (Bitcoin Core, rust-silentpayments, payjoin-rust, BDK, Kyoto, BTCPay Server) -- The PR must be non-trivial: a bug fix, feature implementation, test improvement, or significant documentation addition -- Write a 1,000-word technical report explaining: what you changed, why it matters, what you learned from the review process -- Present your PR and the review process to the cohort - -**Rubric:** - -| Criteria | Points | -|----------|--------| -| PR is well-scoped and clearly described | 20 | -| Code quality: tests, documentation, style compliance | 25 | -| Technical report depth and clarity | 20 | -| Response to reviewer feedback | 15 | -| Presentation quality | 10 | -| PR merged (bonus) | 10 | -| **Total** | **100** | - -**Example projects:** -- Add label support to a Silent Payments wallet implementation -- Improve scanning performance in rust-silentpayments with batch ECDH -- Add BIP77 (Payjoin V2) support to an existing wallet -- Implement coin selection privacy scoring in BDK -- Add compact block filter support to a light client library -- Write integration tests for edge cases in payjoin-rust - ---- - -### Track B: Privacy Analysis Tool - -**Goal:** Build a tool that helps developers or users understand Bitcoin privacy. - -**Requirements:** -- Build a functional tool (CLI, web app, or library) -- The tool must analyze or improve some aspect of Bitcoin privacy -- Write a 1,500-word report documenting: the problem it solves, how it works, privacy model and limitations -- Open-source the code under CC0 or MIT license -- Present a live demo to the cohort - -**Rubric:** - -| Criteria | Points | -|----------|--------| -| Tool solves a real privacy problem | 25 | -| Implementation quality and completeness | 25 | -| Technical report depth | 20 | -| Privacy model analysis (what it protects against, what it doesn't) | 15 | -| Presentation and demo | 15 | -| **Total** | **100** | - -**Example projects:** -- A wallet fingerprint detector: input a raw transaction, output which wallet likely created it and what privacy leaks exist -- A UTXO privacy scorer: given a wallet's UTXO set, score how "private" each UTXO is and recommend actions -- A Payjoin simulator: visual tool showing how Payjoin breaks CIOH with configurable parameters -- A Silent Payment address generator/validator with full BIP352 compliance testing -- A compact block filter explorer: visualize filter contents, false positive rates, compression ratios -- A transaction graph analyzer: given a set of transactions, build and visualize the entity graph using heuristics - ---- - -### Track C: Research and Documentation - -**Goal:** Produce a comprehensive technical document that advances understanding of a Bitcoin privacy topic. - -**Requirements:** -- Write a 3,000-5,000-word technical document -- Must include original analysis (not just summarizing existing material) -- Must reference at least 5 primary sources (BIPs, academic papers, implementation code) -- Publish under CC0 license -- Present findings to the cohort - -**Rubric:** - -| Criteria | Points | -|----------|--------| -| Originality of analysis | 25 | -| Technical depth and accuracy | 25 | -| Quality of writing and structure | 20 | -| Practical recommendations for developers | 15 | -| Presentation quality | 15 | -| **Total** | **100** | - -**Example projects:** -- "Silent Payments Scanning Optimization: A Comparison of Approaches" - benchmark different scanning strategies, analyze trade-offs for mobile vs desktop -- "Payjoin Adoption Analysis: What Needs to Happen for Critical Mass" - study current adoption, identify barriers, propose solutions -- "Wallet Privacy Audit: Comparing Fingerprints Across the Top 10 Bitcoin Wallets" - test real wallets and document their privacy properties -- "The Economics of CoinJoin: How CISA Would Change the Privacy Landscape" - model fee savings and adoption incentives -- "Compact Block Filters for Silent Payments: Architecture Proposals for Mobile Wallets" - design a practical architecture combining CBF + SP -- "Bitcoin Privacy in Southeast Asia: Regulatory Landscape and Developer Opportunities" - map the regulatory environment and identify where privacy tools are most needed - ---- - -### Track D: Education and Curriculum - -**Goal:** Create educational material that extends this curriculum or makes it accessible to a new audience. - -**Requirements:** -- Create a complete lesson, exercise set, or tutorial -- Must be technically accurate and tested -- Must fill a gap in the existing curriculum -- CC0 licensed -- Present and teach a sample lesson to the cohort - -**Rubric:** - -| Criteria | Points | -|----------|--------| -| Educational value and clarity | 25 | -| Technical accuracy | 25 | -| Completeness (ready to use by another facilitator) | 20 | -| Quality of exercises and assessments | 15 | -| Teaching demonstration | 15 | -| **Total** | **100** | - -**Example projects:** -- A visual/interactive tutorial explaining Silent Payments using animations -- An exercise set teaching Payjoin integration in a specific programming language (Go, TypeScript, etc.) -- A "Bitcoin Privacy for Non-Developers" workshop module for activists, journalists, and NGO workers -- A Rust-language version of the Silent Payments sender/scanner exercises -- An advanced module on Taproot privacy properties (MuSig2, threshold signatures, scriptless scripts) -- A module on Lightning Network privacy (channel graph analysis, probing attacks, PTLCs) - ---- - -## Proposal Template - -Submit this before starting work (1 page max): - -``` -# Capstone Proposal - -**Name:** -**Track:** A / B / C / D -**Title:** - -## Problem -What specific problem does this project address? (2-3 sentences) - -## Approach -How will you solve it? What technologies/tools will you use? (3-5 sentences) - -## Deliverables -What exactly will you produce? Be specific. (Bullet list) - -## Timeline -Week 1: ___ -Week 2: ___ -Week 3: ___ -Week 4: ___ - -## Why It Matters -Who benefits from this work? How does it advance Bitcoin privacy? (2-3 sentences) -``` - ---- - -## Presentation Guidelines - -Each capstone presentation should be 10-15 minutes: - -- 2 minutes: problem statement and motivation -- 5-8 minutes: what you built/wrote and how it works (live demo for Tracks A/B) -- 3-5 minutes: what you learned, what was hard, what you'd do differently - -The audience should ask questions. This is a peer review, not a lecture. - ---- - -## Past Capstone Examples - -*(This section will be populated as cohorts complete the track. First cohort graduates become examples for the next.)* - ---- - -*Code Orange Dev School | [codeorange.dev](https://codeorange.dev) | CC0 1.0 Universal* diff --git a/privacy-track/labs/chain-analysis/README.md b/privacy-track/labs/chain-analysis/README.md new file mode 100644 index 0000000..cfe85e4 --- /dev/null +++ b/privacy-track/labs/chain-analysis/README.md @@ -0,0 +1,19 @@ +# Chain Analysis Lab (S4) + +Play the surveillance firm. Implement the heuristics analysts use on sample transactions, then see how far they get and where they break. + +```bash +python3 chain_analysis_lab.py +``` + +| Exercise | Heuristic | +|---|---| +| `apply_cioh` | Common-input-ownership: all inputs belong to one entity | +| `detect_change_output` | Script-type match, round amounts, unnecessary inputs | +| `fingerprint_wallet` | Version, nLockTime, script types, fee rate, output count | +| `detect_coinjoin` | Equal-value outputs and anonymity sets | +| `trace_entity` | Follow an address through the graph | + +Unimplemented functions are skipped, so you can go one at a time. The sample data is synthetic. In the session, test your `fingerprint_wallet` guesses against the real transactions the room builds, and trust the real ones over the hints. + +No dependencies. Python 3.8+. diff --git a/privacy-track/phase-1-foundations/session-01-why-privacy-matters/chain_analysis_lab.py b/privacy-track/labs/chain-analysis/chain_analysis_lab.py similarity index 97% rename from privacy-track/phase-1-foundations/session-01-why-privacy-matters/chain_analysis_lab.py rename to privacy-track/labs/chain-analysis/chain_analysis_lab.py index 8261ab3..4bf6e68 100644 --- a/privacy-track/phase-1-foundations/session-01-why-privacy-matters/chain_analysis_lab.py +++ b/privacy-track/labs/chain-analysis/chain_analysis_lab.py @@ -1,5 +1,5 @@ """ -Bitcoin Privacy Track — Module 1: Chain Analysis Lab +Bitcoin Privacy Track - S4 Think Like the Adversary: Chain Analysis Lab Code Orange Dev School | codeorange.dev Hands-on chain analysis exercise: parse real Bitcoin transactions, @@ -10,13 +10,12 @@ Prerequisites: - Python 3.8+ -- pip install requests (for fetching from mempool.space API) - Understanding of Bitcoin transaction structure (inputs, outputs, scripts) Instructions: - Complete each function by filling in the code where indicated - Run: python3 chain_analysis_lab.py -- Bring your results to the weekly session +- Bring your results to the next session """ import json @@ -322,15 +321,17 @@ def fingerprint_wallet(tx: Transaction) -> Dict[str, str]: # - Version 2: most modern wallets (Bitcoin Core, BDK-based, etc.) # # 2. nLockTime: - # - 0: Electrum, many mobile wallets, some hardware wallet software - # - Current block height (> 500,000 and recent): Bitcoin Core (anti-fee-sniping) + # - 0: many mobile wallets and libraries that don't do anti-fee-sniping + # - Current block height (> 500,000 and recent): anti-fee-sniping, used by + # Bitcoin Core, Electrum, Sparrow and others (sometimes randomized a few + # blocks back). Verify against real txs in S4's fingerprint table! # - Non-zero but not block height: unusual, possibly custom software # # 3. INPUT SCRIPT TYPES: # - p2pkh only: very old wallet or intentionally legacy # - p2sh-p2wpkh: transitional period wallet (2017-2020 era) # - p2wpkh: modern SegWit wallet - # - p2tr: cutting-edge Taproot wallet (Bitcoin Core 24+, Sparrow, etc.) + # - p2tr: Taproot wallet (Bitcoin Core descriptor wallets, Sparrow, BDK-based...) # - Mixed types: wallet migration or consolidation # # 4. FEE RATE: @@ -605,7 +606,7 @@ def run_analysis(): """) print("=" * 70) - print("Done! Bring your code and reflection answers to the weekly call.") + print("Done! Bring your code and reflection answers to the next session.") print("=" * 70) diff --git a/privacy-track/labs/coin-selection/README.md b/privacy-track/labs/coin-selection/README.md new file mode 100644 index 0000000..1f9d5d4 --- /dev/null +++ b/privacy-track/labs/coin-selection/README.md @@ -0,0 +1,11 @@ +# Coin Selection Lab (S5) + +Implement four coin-selection strategies (largest-first, branch-and-bound, privacy-optimized, random), run them against realistic UTXO sets, and score each one on fees *and* privacy. + +```bash +python3 coin_selection_simulator.py +``` + +The challenge: beat branch-and-bound on privacy without paying much more in fees. Then compare what you built with Bitcoin Core (BnB, CoinGrinder, Knapsack and SRD, picking the lowest waste) and [BDK's coin selection](https://github.com/bitcoindevkit/bdk_wallet). + +No dependencies. Python 3.8+. diff --git a/privacy-track/phase-1-foundations/session-03-utxo-management/coin_selection_simulator.py b/privacy-track/labs/coin-selection/coin_selection_simulator.py similarity index 99% rename from privacy-track/phase-1-foundations/session-03-utxo-management/coin_selection_simulator.py rename to privacy-track/labs/coin-selection/coin_selection_simulator.py index e084cb5..4d9264b 100644 --- a/privacy-track/phase-1-foundations/session-03-utxo-management/coin_selection_simulator.py +++ b/privacy-track/labs/coin-selection/coin_selection_simulator.py @@ -1,5 +1,5 @@ """ -Bitcoin Privacy Track — Module 4, Session 1: Coin Selection Simulator +Bitcoin Privacy Track - S5 Coin Selection & Change: Coin Selection Simulator Code Orange Dev School | codeorange.dev Build and compare coin selection algorithms, measuring their privacy @@ -15,7 +15,7 @@ Instructions: - Complete each function by filling in the code where indicated - Run: python3 coin_selection_simulator.py -- Bring your results and analysis to the weekly session +- Bring your results and analysis to the next session """ import random @@ -536,5 +536,5 @@ def run_comparison(utxos: List[UTXO], targets: List[int], fee_rate: float = 5.0) """) print("=" * 70) - print("Done! Bring your results to the weekly session.") + print("Done! Bring your results to the next session.") print("=" * 70) diff --git a/privacy-track/labs/compact-block-filters/README.md b/privacy-track/labs/compact-block-filters/README.md new file mode 100644 index 0000000..16e8c34 --- /dev/null +++ b/privacy-track/labs/compact-block-filters/README.md @@ -0,0 +1,11 @@ +# Compact Block Filters Lab (S7) + +Build a simplified BIP158 filter from scratch: hash elements into a range, Golomb-Rice encode the sorted deltas, match queries against the filter, and measure false positives. Then see how an SP light client uses filters to fetch only the blocks it needs (tie-in to S2). + +```bash +python3 compact_block_filters.py +``` + +**Simplification:** real BIP158 hashes with SipHash-2-4 keyed by the block hash. This lab substitutes a SHA256-based hash so there's nothing to install. The parameters (P = 19, M = 784931) and the Golomb-Rice coding match the spec. Read [BIP158](https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki) for the exact construction, and [Kyoto](https://github.com/2140-dev/kyoto) for a production client. + +No dependencies. Python 3.8+. diff --git a/privacy-track/module-04-wallet-privacy/exercises/compact_block_filters.py b/privacy-track/labs/compact-block-filters/compact_block_filters.py similarity index 98% rename from privacy-track/module-04-wallet-privacy/exercises/compact_block_filters.py rename to privacy-track/labs/compact-block-filters/compact_block_filters.py index 453f65c..6ea41c7 100644 --- a/privacy-track/module-04-wallet-privacy/exercises/compact_block_filters.py +++ b/privacy-track/labs/compact-block-filters/compact_block_filters.py @@ -1,5 +1,5 @@ """ -Bitcoin Privacy Track — Module 4, Session 2: Compact Block Filters +Bitcoin Privacy Track - S7 Light Clients & Your Own Node: Compact Block Filters Code Orange Dev School | codeorange.dev Implement a simplified compact block filter (BIP158) from scratch, @@ -10,7 +10,7 @@ Prerequisites: - Understanding of hash functions and probabilistic data structures -- Completed Module 2 (Silent Payments scanning) +- Helpful: the Silent Payments scanning lab (S2) - Python 3.8+ References: @@ -20,7 +20,7 @@ Instructions: - Complete each function by filling in the code where indicated - Run: python3 compact_block_filters.py -- Bring your results to the weekly session +- Bring your results to the next session """ import hashlib @@ -514,5 +514,5 @@ def simulate_sp_scanning_with_filters(): efficiency? Why or why not? """) print("=" * 70) - print("Done! Bring your results to the weekly session.") + print("Done! Bring your results to the next session.") print("=" * 70) diff --git a/privacy-track/labs/network-privacy/README.md b/privacy-track/labs/network-privacy/README.md new file mode 100644 index 0000000..2b7776f --- /dev/null +++ b/privacy-track/labs/network-privacy/README.md @@ -0,0 +1,103 @@ +# Network Privacy Lab (S8): Wiretap Yourself + +Three experiments on your own Bitcoin Core node: see what BIP324 hides from your ISP, broadcast a transaction without revealing your IP, and measure how many of your peers sit on the same network operator. + +**Needs:** Bitcoin Core **31.0 or later**, [Tor](https://www.torproject.org/download/) running locally (SOCKS on `127.0.0.1:9050`), `jq`, and Wireshark (or `tcpdump`). Everything below uses **signet** (P2P port 38333), with funds from a signet faucet. + +> Facilitators: run it end to end the day before. Signet has far fewer Tor/I2P peers than mainnet, so private broadcast can take a while to find one. If it stalls, that's worth discussing in its own right. + +## 0. Setup + +`~/.bitcoin/bitcoin.conf` (on macOS: `~/Library/Application Support/Bitcoin/bitcoin.conf`): + +```ini +signet=1 +[signet] +proxy=127.0.0.1:9050 # reach onion peers through Tor +listenonion=1 +``` + +```bash +bitcoind -daemon +alias bcli='bitcoin-cli -signet' +bcli getnetworkinfo | jq '.networks[] | {name, reachable}' +``` + +`onion` should be `reachable: true`. If it isn't, fix Tor before going further, because experiments 2 and 3 depend on it. + +## 1. BIP324: what does your ISP see? + +```bash +bcli getpeerinfo | jq -r '.[] | [.addr, .network, .transport_protocol_type] | @tsv' +``` + +`v2` means the connection is encrypted (BIP324, on by default since Core 27). `v1` means plaintext. + +**Capture both:** +1. Start Wireshark on your network interface with the filter `tcp.port == 38333`. Leave it running for a minute. Pick a `v2` peer and look at the payload: it's random-looking bytes, with no `version`, `inv` or `tx` strings +2. Restart with `-v2transport=0` (`bcli stop`, then `bitcoind -daemon -v2transport=0`) and capture again. Now look for the ASCII message names (`version`, `inv`, `tx`) in plaintext. Anyone on the path can read which transactions you announce +3. Restart without the flag afterwards + +**Discuss:** BIP324 hides message contents from the *network path*. What does it *not* hide from the peer you're talking to? And what can your ISP still learn from traffic timing and volume? + +## 2. Private broadcast: send without revealing your IP + +In Core 31, `-privatebroadcast` applies to the **`sendrawtransaction` RPC only**. Wallet sends are unaffected. So we build the transaction in the wallet but broadcast it ourselves. + +```bash +bcli stop && bitcoind -daemon -privatebroadcast=1 +bcli createwallet s8 # skip if you already have a funded signet wallet +bcli -rpcwallet=s8 getnewaddress # fund this from a signet faucet, wait for 1 confirmation + +# Build and sign, but don't broadcast or add to the wallet: +HEX=$(bcli -rpcwallet=s8 -named send \ + outputs='[{"'$(bcli -rpcwallet=s8 getnewaddress)'": 0.0001}]' \ + add_to_wallet=false fee_rate=2 | jq -r .hex) + +bcli sendrawtransaction "$HEX" +bcli getprivatebroadcastinfo +``` + +Watch `getprivatebroadcastinfo` until the transaction leaves the queue, then find it on a signet explorer. + +**What happened:** Core opened a fresh, short-lived connection to a Tor or I2P peer, handed over this one transaction, and closed the connection. The transaction didn't go into your own mempool first. Recipients never see your IP, and two transactions you broadcast this way can't be linked by connection. + +**Discuss:** why one connection per transaction? What would an observer learn if you broadcast your wallet's transactions normally, from the same IP, for a year? + +## 3. ASmap: how surrounded are you? + +An eclipse attacker who controls one big hosting provider can fill many of your peer slots. First count the operators among your current peers. + +**Before**, look up each IPv4/IPv6 peer's AS with Team Cymru's public whois: + +```bash +bcli getpeerinfo | jq -r '.[] | select(.network=="ipv4" or .network=="ipv6") | .addr' \ + | sed -E 's/^\[?([^]]+)\]?:[0-9]+$/\1/' \ + | while read ip; do whois -h whois.cymru.com " -v $ip" | tail -1; done +``` + +Count how many peers share an AS. + +**After**, restart with the asmap data embedded in Core 31 (it's off by default): + +```bash +bcli stop && bitcoind -daemon -asmap=1 +# wait a few minutes for outbound connections to fill +bcli getpeerinfo | jq -r '.[] | .mapped_as // "none"' | sort | uniq -c | sort -rn +``` + +With asmap on, Core buckets peers by AS instead of by IP range, and `getpeerinfo` shows each peer's `mapped_as`. + +**Discuss:** compare the diversity before and after. Why is asmap off by default? (Think about who produces the map, how it goes stale, and whether every node should use the same one.) + +## Proof of work (pick one) + +- Write up your AS-diversity numbers from before and after, as a short guide for your community +- Test an open private-broadcast follow-up PR on your platform and post a precise test report (see the [issue pool](../../ISSUE_POOL.md) and [PR checklist](../../PR_CHECKLIST.md)) +- Take a [peer-observer](https://github.com/peer-observer/peer-observer) beginner issue + +## References + +- [Bitcoin Core 31.0 release notes](https://bitcoincore.org/en/releases/31.0/): `-privatebroadcast`, embedded asmap +- [BIP324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki) · [Core `doc/tor.md`](https://github.com/bitcoin/bitcoin/blob/master/doc/tor.md) +- [Optech: Eclipse attacks](https://bitcoinops.org/en/topics/eclipse-attacks/) diff --git a/privacy-track/labs/payjoin/README.md b/privacy-track/labs/payjoin/README.md new file mode 100644 index 0000000..035fe2f --- /dev/null +++ b/privacy-track/labs/payjoin/README.md @@ -0,0 +1,61 @@ +# Payjoin Lab (S6): Paying Together + +Do an async payjoin (BIP77) between two wallets, then play the chain analyst and catch the common-input heuristic getting it wrong. + +**Needs:** Docker, [`nigiri`](https://github.com/vulpemventures/nigiri) (one-command regtest), Rust/`cargo`, `jq`. + +## 1. Do the payjoin + +Follow the official [payjoin-cli Quick Start](https://github.com/payjoin/rust-payjoin/tree/master/payjoin-cli#quick-start). It's maintained upstream, so it stays current. In short: + +1. `nigiri start`, then create and fund two wallets: `sender` and `receiver` +2. `cargo install payjoin-cli`, and write a `config.toml` for each side +3. In `receiver/`: `payjoin-cli receive 10000`, which prints a BIP21 URI with a `pj=` parameter +4. In `sender/`: `payjoin-cli send "" --fee-rate 1` +5. Mine a block: `nigiri rpc generatetoaddress 1 $(nigiri rpc -rpcwallet=sender getnewaddress)` + +Work in pairs: one person runs the receiver, the other the sender, then swap. With v2 the receiver can go offline between steps 3 and 4. Try it and see. + +## 2. Find the transaction + +```bash +TXID=$(nigiri rpc -rpcwallet=receiver listtransactions "*" 1 | jq -r '.[0].txid') +nigiri rpc getrawtransaction "$TXID" true | jq '{vin: [.vin[] | {txid, vout}], vout: [.vout[] | {value, address: .scriptPubKey.address}]}' +``` + +## 3. Play analyst: apply the common-input heuristic + +The analyst's rule: *all inputs belong to the payer*. Test it. For each input, find which wallet owned the coin it spent: + +```bash +for row in $(nigiri rpc getrawtransaction "$TXID" true | jq -r '.vin[] | "\(.txid):\(.vout)"'); do + prev=${row%:*}; n=${row#*:} + addr=$(nigiri rpc getrawtransaction "$prev" true | jq -r ".vout[$n].scriptPubKey.address") + for w in sender receiver; do + [ "$(nigiri rpc -rpcwallet=$w getaddressinfo "$addr" | jq -r .ismine)" = "true" ] && echo "input $row -> $w" + done +done +``` + +You should see at least one input from **each** wallet. An analyst applying the heuristic would lump the receiver's coin in with the sender's. + +**Then answer, in pairs:** +1. Which output is the payment and which is change? Could an outsider tell? (Hint: the receiver's output now includes their own input's value, so the "payment amount" on-chain is wrong too) +2. Compare with a normal send between the same two wallets. What would the analyst conclude there? +3. Payjoin helps even people who never use it. Why? (What happens to the heuristic's reliability across the whole chain once some share of transactions are payjoins?) +4. What does the payjoin directory learn? What does the OHTTP relay learn? Why use both? + +## 4. Bonus: run the S4 lab on it + +Feed this transaction's shape into your `apply_cioh` and `detect_change_output` from the [chain-analysis lab](../chain-analysis/). Does your own analyst code get fooled? + +## Proof of work (pick one) + +- A rust-payjoin beginner issue from the [issue pool](../../ISSUE_POOL.md). Docs count +- Something in the quick start confused you? Fix the docs upstream, following the [PR checklist](../../PR_CHECKLIST.md) +- Test an open rust-payjoin PR on your platform and post a precise test report + +## References + +- [BIP77: Async Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0077.md) · [BIP78](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki) +- [payjoin.org](https://payjoin.org): how it works, and which wallets support it diff --git a/privacy-track/labs/silent-payments/README.md b/privacy-track/labs/silent-payments/README.md new file mode 100644 index 0000000..b101a17 --- /dev/null +++ b/privacy-track/labs/silent-payments/README.md @@ -0,0 +1,60 @@ +# Silent Payments Lab (BIP352) + +Build a working Silent Payments sender and scanner in plain Python, then check it against the **official BIP352 test vectors**: the same 28 cases Bitcoin Core, libsecp256k1, rust-silentpayments, BDK and every SP wallet test against. When your code passes all of them, it's compatible with real wallets. + +No dependencies. Python 3.8+. Works offline. + +```bash +cd privacy-track/labs/silent-payments +python3 run_vectors.py send # Session 1: grade send.py +python3 run_vectors.py receive # Session 2: grade receive.py +python3 scan_benchmark.py # Session 2: why do indexers exist? +``` + +## What's in here + +| File | You... | What it is | +|---|---|---| +| `send.py` | **edit** | Session 1: 5 exercises, from address decoding to output creation | +| `receive.py` | **edit** | Session 2: 3 core exercises + 2 stretch (labels, spending) | +| `run_vectors.py` | run | Grades your code case by case and points at the exercise that's wrong | +| `scan_benchmark.py` | run | Times your scanner and scales it up to a day of mainnet | +| `secp.py`, `bech32m.py` | read | Tiny secp256k1 and bech32m, just enough for the lab | +| `txin.py` | read | Which inputs count, and how their keys are pulled from scripts. Worth reading: it's where real implementations disagree (see the malleated-P2PKH vector and [bitcoin/bitcoin#36338](https://github.com/bitcoin/bitcoin/pull/36338)) | +| `solutions/` | peek later | Reference solutions. `--solution` runs them through the grader | +| `vectors/` | - | `send_and_receive_test_vectors.json` from [bitcoin/bips](https://github.com/bitcoin/bips/tree/master/bip-0352) (BSD-2-Clause) | + +## Progress looks like this + +``` + ✓ 1 Simple send: two inputs + ✓ 7 Single recipient: taproot only inputs with even y-values + ✗ 8 Single recipient: taproot only with mixed even/odd y-values + Exercise 2: a_sum is wrong (did you negate odd-y taproot keys?) + · 13 Receiving with labels: label with even parity + not implemented yet: generate_label() + +21/28 vectors passing +``` + +Add `-v` to see got/expected values when a case fails. + +## The bugs everyone hits (read after you're stuck, not before) + +1. **Serializing the shared secret as x-only.** `t_k` hashes the **33-byte compressed** point. Using 32 bytes passes zero vectors. +2. **Forgetting taproot negation.** An x-only key means the even-y point. If your taproot private key gives odd y, negate it. +3. **Comparing outputs by position.** The receiver must check `P_k` against **every** output. Senders shuffle outputs, and they should. +4. **Stopping after one match.** One transaction can pay you several times (`k = 0, 1, 2...`). +5. **Sorting outpoints by (txid, vout) as numbers.** BIP352 compares the **serialized** bytes, where the txid is little-endian and vout is little-endian. Vector 5 exists to catch this. + +## Safety + +This is teaching code: slow and not constant-time. **Never paste real keys into it.** Use signet or regtest for anything on-chain. + +## Going further + +- [BIP352](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) and its [reference.py](https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py): compare your code line by line +- [libsecp256k1 `silentpayments` module](https://github.com/bitcoin-core/secp256k1) (merged July 2026, PR #1765) and the light-client API follow-up (#1912) +- [Bitcoin Core tracking issue #28536](https://github.com/bitcoin/bitcoin/issues/28536): BIP352 core logic merged in #35301; sending (#35302) and receiving (#32966) in review +- [rust-silentpayments](https://github.com/cygnet3/rust-silentpayments), [bdk-sp](https://github.com/bitcoindevkit/bdk-sp), [BlindBit Oracle](https://github.com/setavenger/blindbit-oracle), [Frigate](https://github.com/sparrowwallet/frigate), [Dana wallet](https://github.com/cygnet3/danawallet), [Shroud](https://github.com/CypherCommons/shroud) +- [Bitcoin Optech: Silent Payments](https://bitcoinops.org/en/topics/silent-payments/): history and news diff --git a/privacy-track/labs/silent-payments/bech32m.py b/privacy-track/labs/silent-payments/bech32m.py new file mode 100644 index 0000000..5be1009 --- /dev/null +++ b/privacy-track/labs/silent-payments/bech32m.py @@ -0,0 +1,73 @@ +""" +Bech32m encoding for Silent Payment addresses. PROVIDED - you don't edit this. + +An SP address is hrp + "1" + bech32m(version || B_scan || B_spend) + hrp "sp" on mainnet, "tsp" on testnet/signet/regtest + version 0 (a single 5-bit character, "q") + payload 33-byte B_scan followed by 33-byte B_spend (66 bytes) + +SP addresses are ~116 characters, so BIP352 raises bech32's 90-character +limit to 1023. +""" + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2BC830A3 +MAX_LEN = 1023 + + +def _polymod(values): + gen = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] + chk = 1 + for v in values: + top = chk >> 25 + chk = (chk & 0x1FFFFFF) << 5 ^ v + for i in range(5): + chk ^= gen[i] if ((top >> i) & 1) else 0 + return chk + + +def _hrp_expand(hrp): + return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp] + + +def _convertbits(data, frombits, tobits, pad): + acc, bits, out, maxv = 0, 0, [], (1 << tobits) - 1 + for value in data: + acc = (acc << frombits) | value + bits += frombits + while bits >= tobits: + bits -= tobits + out.append((acc >> bits) & maxv) + if pad and bits: + out.append((acc << (tobits - bits)) & maxv) + elif not pad and (bits >= frombits or ((acc << (tobits - bits)) & maxv)): + raise ValueError("invalid padding") + return out + + +def encode(hrp: str, version: int, payload: bytes) -> str: + data = [version] + _convertbits(payload, 8, 5, True) + values = _hrp_expand(hrp) + data + polymod = _polymod(values + [0] * 6) ^ BECH32M_CONST + checksum = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + return hrp + "1" + "".join(CHARSET[d] for d in data + checksum) + + +def decode(addr: str): + """Return (hrp, version, payload_bytes). Raises ValueError if invalid.""" + if addr.lower() != addr and addr.upper() != addr: + raise ValueError("mixed case") + addr = addr.lower() + if len(addr) > MAX_LEN: + raise ValueError("too long") + pos = addr.rfind("1") + if pos < 1 or pos + 7 > len(addr): + raise ValueError("bad separator position") + hrp, rest = addr[:pos], addr[pos + 1:] + if any(c not in CHARSET for c in rest): + raise ValueError("invalid character") + data = [CHARSET.find(c) for c in rest] + if _polymod(_hrp_expand(hrp) + data) != BECH32M_CONST: + raise ValueError("bad checksum (is this bech32m?)") + version, payload = data[0], bytes(_convertbits(data[1:-6], 5, 8, False)) + return hrp, version, payload diff --git a/privacy-track/labs/silent-payments/receive.py b/privacy-track/labs/silent-payments/receive.py new file mode 100644 index 0000000..154e993 --- /dev/null +++ b/privacy-track/labs/silent-payments/receive.py @@ -0,0 +1,115 @@ +""" +Session 2 lab - build a Silent Payments RECEIVER / scanner (BIP352) + +Grade yourself against the official BIP352 test vectors: + + python3 run_vectors.py receive + +You don't need to have finished Session 1: the grader computes the input hash +for you (that's the sender-side math). + +Same toolbox as send.py: G, N, Point, a * G, P + Q, P - Q, -P, P.to_bytes(), +P.to_xonly(), Point.from_bytes(b), tagged_hash(tag, m). + +Spec: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki + (sections "Scanning" and "Labels") +""" + +from secp import G, N, Point, tagged_hash +import bech32m + +K_MAX = 2323 + + +# --------------------------------------------------------------------------- +# Exercise 1 - Make your address +# +# bech32m.encode(hrp, 0, B_scan.to_bytes() + B_spend.to_bytes()) +# --------------------------------------------------------------------------- +def encode_sp_address(B_scan: Point, B_spend: Point, hrp: str = "sp") -> str: + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 2 - The tweak (what an indexer serves you) +# +# For every transaction, the receiver needs input_hash * A_sum. +# This doesn't depend on your keys at all, so a server can precompute it +# for every tx in every block and hand it to light clients. That's exactly +# what BlindBit Oracle, Frigate, and Bitcoin Core's upcoming index do. +# --------------------------------------------------------------------------- +def compute_tweak(A_sum: Point, input_hash: int) -> Point: + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 3 - Scan one transaction +# +# ecdh_shared_secret = b_scan * tweak +# k = 0 +# loop: +# t_k = tagged_hash("BIP0352/SharedSecret", ser_P(ecdh_shared_secret) || ser_32(k)) +# P_k = B_spend + t_k * G +# if P_k.to_xonly() equals ANY remaining output: +# record {"pub_key": out.hex(), "priv_key_tweak": t_k as 32-byte hex} +# remove that output, k += 1, keep looping +# else: stop +# (never go past K_MAX) +# +# Two classic bugs: checking only outputs[k] (outputs can be in any order!), +# and stopping after the first match (one tx can pay you several times). +# +# Ignore `labels` until Exercise 4. +# --------------------------------------------------------------------------- +def scan(b_scan: int, B_spend: Point, tweak: Point, outputs, labels=None): + """outputs: list of 32-byte x-only keys. Returns a list of dicts.""" + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 4 - Labels (stretch) +# +# Labels let one wallet hand out distinguishable addresses (donations vs +# invoices vs change) while scanning only once. +# +# label_m = tagged_hash("BIP0352/Label", ser_256(b_scan) || ser_32(m)) as int +# B_m = B_spend + label_m * G <- the labeled address's spend key +# +# Then extend scan(): `labels` maps label_point.to_bytes() -> label_m. +# When an output doesn't equal P_k, it may equal P_k + label_m * G. Because +# the output is x-only you don't know its y, so for O = Point.from_bytes(out) +# check both O - P_k and -O - P_k against `labels`. On a hit the +# priv_key_tweak is (t_k + label_m) mod N. +# --------------------------------------------------------------------------- +def generate_label(b_scan: int, m: int) -> int: + # YOUR CODE HERE + return None + + +def labeled_spend_key(b_scan: int, B_spend: Point, m: int) -> Point: + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 5 - Prove you can spend it (stretch) +# +# d = b_spend + priv_key_tweak (mod N). Taproot needs the even-y key, so if +# d * G has odd y, return N - d instead. +# --------------------------------------------------------------------------- +def spend_key(b_spend: int, priv_key_tweak: int) -> int: + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Discussion (bring to the call): +# - Your scan key can find payments but not spend them. What can someone +# who steals b_scan learn? What can't they do? +# - Exercise 2 runs on a server. What does that server learn about you? +# Compare: downloading tweaks (BlindBit) vs sending b_scan (Frigate). +# - Run scan_benchmark.py. How long for one day of mainnet on your laptop? +# --------------------------------------------------------------------------- diff --git a/privacy-track/labs/silent-payments/run_vectors.py b/privacy-track/labs/silent-payments/run_vectors.py new file mode 100644 index 0000000..6020235 --- /dev/null +++ b/privacy-track/labs/silent-payments/run_vectors.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Grade your Silent Payments code against the official BIP352 test vectors. + + python3 run_vectors.py send # grade send.py (Session 1) + python3 run_vectors.py receive # grade receive.py (Session 2) + python3 run_vectors.py send --solution # run the reference solution instead + python3 run_vectors.py send -v # show got/expected on failure + +The vectors are the ones Bitcoin Core, libsecp256k1, rust-silentpayments and +every other implementation test against. Passing them all means your code +interoperates with real wallets. +""" + +import argparse +import importlib.util +import json +import sys +import time +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +from secp import G, N, Point # noqa: E402 +from txin import input_pubkey, serialize_outpoint # noqa: E402 + +VECTORS = HERE / "vectors" / "send_and_receive_test_vectors.json" + + +def load(name, solution): + path = HERE / ("solutions" if solution else "") / f"{name}.py" + spec = importlib.util.spec_from_file_location(f"{name}_under_test", path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +class NotImplementedYet(Exception): + pass + + +def need(value, what): + if value is None: + raise NotImplementedYet(what) + return value + + +def eligible_inputs(vins, with_privkeys): + keys, pubs = [], [] + for vin in vins: + got = input_pubkey(vin) + if got is None: + continue + pub, is_taproot = got + pubs.append(pub) + if with_privkeys: + keys.append((int(vin["private_key"], 16), is_taproot)) + return keys, pubs + + +# --------------------------------------------------------------------------- send + +def grade_send(case, mod, verbose): + for test in case["sending"]: + given, expected = test["given"], test["expected"] + keys, _ = eligible_inputs(given["vin"], with_privkeys=True) + outpoints = [serialize_outpoint(v["txid"], v["vout"]) for v in given["vin"]] + recipients = [] + for r in given["recipients"]: + recipients += [r["address"]] * r.get("count", 1) + + if not keys: # no eligible inputs: a sender must not create SP outputs + if expected["outputs"] != [[]]: + return False, "vector expects outputs but no eligible inputs found" + continue + + # Step-by-step checks give a precise hint about which exercise is wrong. + if "input_private_key_sum" in expected: + a_sum = need(mod.sum_input_private_keys(keys), "sum_input_private_keys") + if a_sum != int(expected["input_private_key_sum"], 16): + return False, "Exercise 2: a_sum is wrong (did you negate odd-y taproot keys?)" + + B_scan0, _ = need(mod.decode_sp_address(recipients[0]), "decode_sp_address") + if B_scan0.to_bytes().hex() != given["recipients"][0]["scan_pub_key"]: + return False, "Exercise 1: decoded B_scan does not match" + + outputs = need(mod.create_outputs(keys, outpoints, recipients), "create_outputs") + got = {o.hex() for o in outputs} + if not any(got == set(option) for option in expected["outputs"]): + msg = "outputs don't match" + if verbose: + msg += f"\n got {sorted(got)}\n expected one of {expected['outputs']}" + return False, msg + return True, "" + + +# ------------------------------------------------------------------------ receive + +def grade_receive(case, mod, verbose): + input_hash = load("send", solution=True).input_hash # sender side is Session 1's job + for test in case["receiving"]: + given, expected = test["given"], test["expected"] + b_scan = int(given["key_material"]["scan_priv_key"], 16) + b_spend = int(given["key_material"]["spend_priv_key"], 16) + B_scan, B_spend = b_scan * G, b_spend * G + + addresses = [need(mod.encode_sp_address(B_scan, B_spend, "sp"), "encode_sp_address")] + labels = {} + for m in given["labels"]: + label = need(mod.generate_label(b_scan, m), "generate_label") + labels[(label * G).to_bytes()] = label + B_m = need(mod.labeled_spend_key(b_scan, B_spend, m), "labeled_spend_key") + addresses.append(mod.encode_sp_address(B_scan, B_m, "sp")) + if addresses != expected["addresses"]: + return False, "Exercise 1/4: your addresses don't match (encoding or labels)" + + _, pubs = eligible_inputs(given["vin"], with_privkeys=False) + found = [] + if pubs: + A_sum = Point.infinity() + for p in pubs: + A_sum = A_sum + p + if A_sum.is_infinity: + continue # receiver must skip this tx + outpoints = [serialize_outpoint(v["txid"], v["vout"]) for v in given["vin"]] + tweak = need(mod.compute_tweak(A_sum, input_hash(outpoints, A_sum)), "compute_tweak") + if "tweak" in expected and tweak.to_bytes().hex() != expected["tweak"]: + return False, "Exercise 2: tweak is wrong" + outputs = [bytes.fromhex(o) for o in given["outputs"]] + found = need(mod.scan(b_scan, B_spend, tweak, outputs, labels or None), "scan") + + if "n_outputs" in expected: + if len(found) != expected["n_outputs"]: + return False, f"Exercise 3: found {len(found)} outputs, expected {expected['n_outputs']}" + continue + want = {(o["pub_key"], o["priv_key_tweak"]) for o in expected["outputs"]} + got = {(o["pub_key"], o["priv_key_tweak"]) for o in found} + if got != want: + hint = "Exercise 4: labels" if given["labels"] else "Exercise 3: scan" + msg = f"{hint} - found {len(got)} outputs, expected {len(want)}" + if verbose: + msg += f"\n got {sorted(got)}\n expected {sorted(want)}" + return False, msg + + # Stretch goal: prove you can actually spend what you found. + for o in found: + d = mod.spend_key(b_spend, int(o["priv_key_tweak"], 16)) + if d is None: + break + if (d * G).to_xonly().hex() != o["pub_key"] or not (d * G).has_even_y(): + return False, "Exercise 5: spend_key doesn't match the output key" + return True, "" + + +# --------------------------------------------------------------------------- main + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("side", choices=["send", "receive"]) + ap.add_argument("--solution", action="store_true", help="grade the reference solution") + ap.add_argument("-v", "--verbose", action="store_true") + args = ap.parse_args() + + mod = load(args.side, args.solution) + grade = grade_send if args.side == "send" else grade_receive + cases = json.loads(VECTORS.read_text()) + + passed = failed = todo = 0 + start = time.time() + for i, case in enumerate(cases, 1): + name = case["comment"] + try: + ok, why = grade(case, mod, args.verbose) + except NotImplementedYet as e: + print(f" · {i:2} {name}\n not implemented yet: {e}()") + todo += 1 + continue + except Exception as e: # a crash in participant code is just a failure + ok, why = False, f"crashed: {type(e).__name__}: {e}" + if ok: + passed += 1 + print(f" ✓ {i:2} {name}") + else: + failed += 1 + print(f" ✗ {i:2} {name}\n {why}") + + total = passed + failed + todo + print(f"\n{passed}/{total} vectors passing ({time.time() - start:.1f}s)") + if passed == total: + print("All green. Your code agrees with every BIP352 implementation. 🟠") + sys.exit(0 if failed == 0 and todo == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/privacy-track/labs/silent-payments/scan_benchmark.py b/privacy-track/labs/silent-payments/scan_benchmark.py new file mode 100644 index 0000000..89d93b5 --- /dev/null +++ b/privacy-track/labs/silent-payments/scan_benchmark.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Session 2: how expensive is scanning? + +A silent payment receiver must do one ECDH (a scalar multiplication) for +EVERY eligible transaction on the chain, because any of them might pay you. +This script times your scan() on synthetic transactions and extrapolates. + + python3 scan_benchmark.py # uses your receive.py if implemented + python3 scan_benchmark.py 500 # benchmark 500 transactions + +Python is ~100-1000x slower than libsecp256k1, so treat the result as an +upper bound - then ask what a phone on a data plan would pay even at C speed. +""" + +import importlib.util +import secrets +import sys +import time +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) +from secp import G, N # noqa: E402 + +# Rough order of magnitude, not a measurement: check a block explorer or an +# indexer's stats for today's number and plug it in. +ELIGIBLE_TXS_PER_DAY = 300_000 + + +def load_scan(): + for folder, label in ((HERE, "your receive.py"), (HERE / "solutions", "the reference solution")): + spec = importlib.util.spec_from_file_location("r", folder / "receive.py") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + k = secrets.randbelow(N - 1) + 1 + if mod.compute_tweak(k * G, 1) is not None and mod.scan(k, k * G, k * G, []) is not None: + return mod, label + raise SystemExit("no scan() implementation found") + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 200 + mod, label = load_scan() + print(f"Benchmarking {label} on {n} synthetic transactions...") + + b_scan = secrets.randbelow(N - 1) + 1 + B_spend = (secrets.randbelow(N - 1) + 1) * G + # Each tx: a random tweak (what an indexer would serve) + 2 random outputs. + txs = [((secrets.randbelow(N - 1) + 1) * G, + [((secrets.randbelow(N - 1) + 1) * G).to_xonly() for _ in range(2)]) + for _ in range(n)] + + start = time.perf_counter() + for tweak, outputs in txs: + mod.scan(b_scan, B_spend, tweak, outputs) + per_tx = (time.perf_counter() - start) / n + + day = per_tx * ELIGIBLE_TXS_PER_DAY + print(f" {per_tx * 1000:.2f} ms per transaction") + print(f" ~{day / 60:.0f} min to scan one day of chain (assuming {ELIGIBLE_TXS_PER_DAY:,} eligible txs/day)") + print(f" ~{day * 365 / 3600:.0f} hours to catch up after a year offline") + print("\nNow discuss: who should do this work - your phone, your node, or a server?") + print("And what does each option reveal about you?") + + +if __name__ == "__main__": + main() diff --git a/privacy-track/labs/silent-payments/secp.py b/privacy-track/labs/silent-payments/secp.py new file mode 100644 index 0000000..0ce3703 --- /dev/null +++ b/privacy-track/labs/silent-payments/secp.py @@ -0,0 +1,134 @@ +""" +Tiny pure-Python secp256k1, just enough for BIP352. PROVIDED - you don't edit this. + +Teaching code: slow and NOT constant-time. Never use it with real keys. + + G the generator point + N the group order (private keys / scalars live in [1, N-1]) + Point a curve point; supports P + Q, P - Q, -P, k * P + Point.infinity() the "zero" point (e.g. the sum of P and -P) + tagged_hash(t, m) BIP340 tagged hash: SHA256(SHA256(t) || SHA256(t) || m) +""" + +import hashlib + +P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + +def tagged_hash(tag: str, msg: bytes) -> bytes: + t = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(t + t + msg).digest() + + +class Point: + __slots__ = ("x", "y") + + def __init__(self, x, y): + self.x = x + self.y = y + + # --- construction ------------------------------------------------- + @staticmethod + def infinity() -> "Point": + return Point(None, None) + + @staticmethod + def lift_x(x: int, even_y: bool = True) -> "Point": + """Find the point with this x coordinate (even y by default, as BIP340).""" + if not 0 <= x < P: + raise ValueError("x out of range") + c = (pow(x, 3, P) + 7) % P + y = pow(c, (P + 1) // 4, P) + if y * y % P != c: + raise ValueError("x is not on the curve") + if (y % 2 == 0) != even_y: + y = P - y + return Point(x, y) + + @staticmethod + def from_bytes(b: bytes) -> "Point": + """Parse a 33-byte compressed (02/03 || x) or 32-byte x-only key.""" + if len(b) == 32: + return Point.lift_x(int.from_bytes(b, "big")) + if len(b) == 33 and b[0] in (2, 3): + return Point.lift_x(int.from_bytes(b[1:], "big"), even_y=(b[0] == 2)) + raise ValueError("expected 32 or 33 bytes of public key") + + # --- properties / serialization ----------------------------------- + @property + def is_infinity(self) -> bool: + return self.x is None + + def has_even_y(self) -> bool: + return self.y % 2 == 0 + + def to_bytes(self) -> bytes: + """33-byte compressed encoding. BIP352 calls this ser_P(P).""" + return (b"\x02" if self.has_even_y() else b"\x03") + self.x.to_bytes(32, "big") + + def to_xonly(self) -> bytes: + """32-byte x-only encoding - what a taproot output commits to.""" + return self.x.to_bytes(32, "big") + + # --- arithmetic --------------------------------------------------- + def __eq__(self, other): + return isinstance(other, Point) and self.x == other.x and self.y == other.y + + def __hash__(self): + return hash((self.x, self.y)) + + def __neg__(self): + return self if self.is_infinity else Point(self.x, (-self.y) % P) + + def __add__(self, other: "Point") -> "Point": + if self.is_infinity: + return other + if other.is_infinity: + return self + if self.x == other.x: + if (self.y + other.y) % P == 0: + return Point.infinity() + lam = 3 * self.x * self.x * pow(2 * self.y, -1, P) % P + else: + lam = (other.y - self.y) * pow(other.x - self.x, -1, P) % P + x3 = (lam * lam - self.x - other.x) % P + return Point(x3, (lam * (self.x - x3) - self.y) % P) + + def __sub__(self, other: "Point") -> "Point": + return self + (-other) + + def __rmul__(self, k: int) -> "Point": + k %= N + if self is G or self == G: + return _mul_G(k) + result, addend = Point.infinity(), self + while k: + if k & 1: + result = result + addend + addend = addend + addend + k >>= 1 + return result + + def __repr__(self): + return "Point(infinity)" if self.is_infinity else f"Point({self.to_bytes().hex()})" + + +G = Point(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, + 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) + +# Precomputed 2^i * G makes k * G about twice as fast (only additions). +_G_TABLE = [G] +for _ in range(255): + _G_TABLE.append(_G_TABLE[-1] + _G_TABLE[-1]) + + +def _mul_G(k: int) -> Point: + result = Point.infinity() + i = 0 + while k: + if k & 1: + result = result + _G_TABLE[i] + k >>= 1 + i += 1 + return result diff --git a/privacy-track/labs/silent-payments/send.py b/privacy-track/labs/silent-payments/send.py new file mode 100644 index 0000000..4ed1083 --- /dev/null +++ b/privacy-track/labs/silent-payments/send.py @@ -0,0 +1,122 @@ +""" +Session 1 lab - build a Silent Payments SENDER (BIP352) + +Fill in the five functions below, then grade yourself against the official +BIP352 test vectors (the same ones Bitcoin Core and every wallet use): + + python3 run_vectors.py send + +Each function returns None until you implement it; the grader marks it +"not implemented yet" and moves on, so you can go one exercise at a time. + +Toolbox (from secp.py): + G, N generator point, group order + a * G scalar times point -> Point + P + Q, P - Q, -P point arithmetic + P.to_bytes() 33-byte compressed key (BIP352's ser_P) + P.to_xonly() 32-byte x-only key (what a taproot output holds) + P.has_even_y() parity of y + Point.from_bytes(b) parse 33-byte or 32-byte key + tagged_hash(tag, m) BIP340-style tagged hash -> 32 bytes + int.from_bytes(b, "big"), n.to_bytes(4, "big") + +Spec: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki + (section "Creating outputs") +""" + +from secp import G, N, Point, tagged_hash +import bech32m + +K_MAX = 2323 # BIP352: max outputs to one recipient (per B_scan) in one tx + + +# --------------------------------------------------------------------------- +# Exercise 1 - Read a silent payment address +# +# An SP address is bech32m: hrp "sp" (mainnet) or "tsp" (test networks), +# version 0, and a 66-byte payload = B_scan (33 bytes) || B_m (33 bytes). +# B_m is the spend key, possibly with a label added (Session 2). +# +# Use bech32m.decode(address) -> (hrp, version, payload). +# Reject anything that isn't hrp sp/tsp, version 0 and 66 bytes. +# --------------------------------------------------------------------------- +def decode_sp_address(address: str): + """Return (B_scan, B_m) as Points.""" + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 2 - Sum the input private keys +# +# `inputs` is a list of (private_key: int, is_taproot: bool) for each +# eligible input. a_sum = a_1 + a_2 + ... (mod N). +# +# Catch: a taproot output only stores an x-only key, which always means the +# EVEN-y point. If a taproot private key a gives an odd-y point (a*G), the +# key actually in use is its negation N - a. Non-taproot keys are used as-is. +# --------------------------------------------------------------------------- +def sum_input_private_keys(inputs) -> int: + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 3 - The input hash +# +# input_hash = tagged_hash("BIP0352/Inputs", outpoint_L || ser_P(A_sum)) +# +# outpoint_L the smallest outpoint, comparing the 36-byte serializations +# byte-by-byte (Python's min() on bytes does exactly that) +# A_sum a_sum * G +# +# Return it as an int. Why it exists: it makes the shared secret unique per +# transaction, so paying the same person twice from the same key does NOT +# produce the same address twice. +# --------------------------------------------------------------------------- +def input_hash(outpoints, A_sum: Point) -> int: + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 4 - One output key +# +# t_k = tagged_hash("BIP0352/SharedSecret", ser_P(ecdh_shared_secret) || ser_32(k)) +# P_k = B_m + t_k * G +# +# ser_P 33-byte COMPRESSED point (not x-only! a classic bug) +# ser_32(k) k as 4 bytes, big-endian +# --------------------------------------------------------------------------- +def output_pubkey(ecdh_shared_secret: Point, B_m: Point, k: int) -> Point: + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Exercise 5 - Put it together +# +# 1. a_sum = sum_input_private_keys(inputs). If it's 0, return [] (can't pay). +# 2. h = input_hash(outpoints, a_sum * G) +# 3. Group recipients by B_scan (same person, several addresses/labels). +# If any group has more than K_MAX members, return []. +# 4. For each group: ecdh_shared_secret = (h * a_sum mod N) * B_scan +# then for k = 0, 1, 2 ... over that group's B_m values: +# output = output_pubkey(ecdh_shared_secret, B_m, k) +# 5. Return the list of outputs as x-only bytes (P.to_xonly()). +# +# `outpoints` is a list of 36-byte serialized outpoints (ALL inputs). +# `recipients` is a list of SP address strings (may repeat). +# --------------------------------------------------------------------------- +def create_outputs(inputs, outpoints, recipients): + # YOUR CODE HERE + return None + + +# --------------------------------------------------------------------------- +# Discussion (bring to the call): +# - The receiver can't know which of your inputs are "yours". Why does BIP352 +# sum ALL eligible inputs instead of using just one? +# - Why must k restart at 0 for each recipient group? +# - What breaks if you pay an SP address from a P2WSH multisig input only? +# --------------------------------------------------------------------------- diff --git a/privacy-track/labs/silent-payments/solutions/receive.py b/privacy-track/labs/silent-payments/solutions/receive.py new file mode 100644 index 0000000..1739f5c --- /dev/null +++ b/privacy-track/labs/silent-payments/solutions/receive.py @@ -0,0 +1,65 @@ +""" +REFERENCE SOLUTION - Silent Payments receiver / scanner (BIP352). + +Passes every receiving vector in vectors/. Run with: + python3 run_vectors.py receive --solution +""" + +from secp import G, N, Point, tagged_hash +import bech32m + +K_MAX = 2323 + + +def encode_sp_address(B_scan: Point, B_spend: Point, hrp: str = "sp") -> str: + return bech32m.encode(hrp, 0, B_scan.to_bytes() + B_spend.to_bytes()) + + +def generate_label(b_scan: int, m: int) -> int: + return int.from_bytes(tagged_hash("BIP0352/Label", b_scan.to_bytes(32, "big") + m.to_bytes(4, "big")), "big") + + +def labeled_spend_key(b_scan: int, B_spend: Point, m: int) -> Point: + return B_spend + generate_label(b_scan, m) * G + + +def compute_tweak(A_sum: Point, input_hash: int) -> Point: + return input_hash * A_sum + + +def scan(b_scan: int, B_spend: Point, tweak: Point, outputs, labels=None): + labels = labels or {} + ecdh_shared_secret = b_scan * tweak + remaining = list(outputs) + found = [] + k = 0 + while k < K_MAX: + t_k = int.from_bytes(tagged_hash("BIP0352/SharedSecret", + ecdh_shared_secret.to_bytes() + k.to_bytes(4, "big")), "big") + P_k = B_spend + t_k * G + match = None + for out in remaining: + if out == P_k.to_xonly(): + match = (out, t_k) + break + if labels: + # The output could be P_k + label*G. The x-only output hides its y, + # so try both candidates: output - P_k and -output - P_k. + O = Point.from_bytes(out) + for diff in (O - P_k, -O - P_k): + if not diff.is_infinity and diff.to_bytes() in labels: + match = (out, (t_k + labels[diff.to_bytes()]) % N) + break + if match: + break + if match is None: + break + remaining.remove(match[0]) + found.append({"pub_key": match[0].hex(), "priv_key_tweak": match[1].to_bytes(32, "big").hex()}) + k += 1 + return found + + +def spend_key(b_spend: int, priv_key_tweak: int) -> int: + d = (b_spend + priv_key_tweak) % N + return d if (d * G).has_even_y() else N - d diff --git a/privacy-track/labs/silent-payments/solutions/send.py b/privacy-track/labs/silent-payments/solutions/send.py new file mode 100644 index 0000000..a5f9fd5 --- /dev/null +++ b/privacy-track/labs/silent-payments/solutions/send.py @@ -0,0 +1,60 @@ +""" +REFERENCE SOLUTION - Silent Payments sender (BIP352). + +Facilitators: this passes every sending vector in vectors/. Participants: +try the exercise in ../send.py first. Run with: python3 run_vectors.py send --solution +""" + +from secp import G, N, Point, tagged_hash +import bech32m + +K_MAX = 2323 # BIP352 per-group recipient limit + + +def decode_sp_address(address: str): + hrp, version, payload = bech32m.decode(address) + if hrp not in ("sp", "tsp"): + raise ValueError("not a silent payment address") + if version != 0 or len(payload) != 66: + raise ValueError("unsupported version or bad length") + return Point.from_bytes(payload[:33]), Point.from_bytes(payload[33:]) + + +def sum_input_private_keys(inputs): + a_sum = 0 + for a, is_taproot in inputs: + if is_taproot and not (a * G).has_even_y(): + a = N - a + a_sum = (a_sum + a) % N + return a_sum + + +def input_hash(outpoints, A_sum: Point) -> int: + smallest = min(outpoints) + return int.from_bytes(tagged_hash("BIP0352/Inputs", smallest + A_sum.to_bytes()), "big") + + +def output_pubkey(ecdh_shared_secret: Point, B_m: Point, k: int) -> Point: + t_k = tagged_hash("BIP0352/SharedSecret", ecdh_shared_secret.to_bytes() + k.to_bytes(4, "big")) + return B_m + int.from_bytes(t_k, "big") * G + + +def create_outputs(inputs, outpoints, recipients): + a_sum = sum_input_private_keys(inputs) + if a_sum == 0: + return [] + h = input_hash(outpoints, a_sum * G) + + groups = {} + for address in recipients: + B_scan, B_m = decode_sp_address(address) + groups.setdefault(B_scan, []).append(B_m) + if any(len(g) > K_MAX for g in groups.values()): + return [] + + outputs = [] + for B_scan, B_ms in groups.items(): + ecdh_shared_secret = (h * a_sum % N) * B_scan + for k, B_m in enumerate(B_ms): + outputs.append(output_pubkey(ecdh_shared_secret, B_m, k).to_xonly()) + return outputs diff --git a/privacy-track/labs/silent-payments/txin.py b/privacy-track/labs/silent-payments/txin.py new file mode 100644 index 0000000..c836d8c --- /dev/null +++ b/privacy-track/labs/silent-payments/txin.py @@ -0,0 +1,158 @@ +""" +Transaction-input plumbing for BIP352. PROVIDED - read it, but you don't edit it. + +BIP352 only uses inputs whose public key can be read straight from the +spending transaction ("eligible inputs"): + + P2TR key from the output itself (x-only, even y) + - skipped if it is a script-path spend whose internal key + is the NUMS point H (a provably unspendable key) + P2WPKH key is the last witness item + P2SH-P2WPKH key is the last witness item + P2PKH key is found in the scriptSig by hashing 33-byte windows + +Anything else (P2WSH multisig, bare scripts, uncompressed keys...) is skipped. +The sender and the receiver MUST agree on this list, or they derive +different shared secrets and the payment is lost. That's why this code is +full of edge cases, and why it's a good place to review upstream PRs. +""" + +import hashlib +import struct +from io import BytesIO + +from secp import Point + +NUMS_H = bytes.fromhex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") + + +def serialize_outpoint(txid_hex: str, vout: int) -> bytes: + """36 bytes: txid in *internal* (little-endian) byte order || vout as uint32 LE. + + txids are displayed big-endian, so the hex string is reversed first. + """ + return bytes.fromhex(txid_hex)[::-1] + struct.pack(" bytes: + sha = hashlib.sha256(b).digest() + try: + return hashlib.new("ripemd160", sha).digest() + except ValueError: # some OpenSSL builds drop ripemd160 + return _ripemd160(sha) + + +def _read_witness(hex_str: str): + f = BytesIO(bytes.fromhex(hex_str)) + + def compact_size(): + n = f.read(1) + if not n: + return 0 + n = n[0] + if n == 253: + return struct.unpack(" 1 and stack[-1][:1] == b"\x50": + stack.pop() # annex + if len(stack) > 1 and stack[-1][1:33] == NUMS_H: + return None # script-path spend with unspendable internal key + try: + return Point.from_bytes(spk[2:]), True + except ValueError: + return None + + return None + + +# --- pure-python RIPEMD160 fallback (only used if hashlib lacks it) --------- +def _ripemd160(msg: bytes) -> bytes: + import struct as s + def rol(x, n): return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF + def f(j, x, y, z): + return [x ^ y ^ z, (x & y) | (~x & z), (x | ~y) ^ z, (x & z) | (y & ~z), x ^ (y | ~z)][j // 16] & 0xFFFFFFFF + K1 = [0, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E] + K2 = [0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0] + R1 = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13] + R2 = [5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11] + S1 = [11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6] + S2 = [8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11] + h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0] + msg = msg + b"\x80" + b"\x00" * ((55 - len(msg)) % 64) + s.pack(" 2 sessions - Understand how Bitcoin surveillance works and why privacy is a protocol concern. - ---- - -## Session 1: How Bitcoin Surveillance Works - -### Overview - -Before you can build privacy tools, you need to understand what you're defending against. This session covers the core heuristics that chain analysis firms use to de-anonymize Bitcoin users, and gives you hands-on experience tracing transactions yourself. - -### Topics - -**Common-Input-Ownership Heuristic (CIOH)** -- If a transaction has multiple inputs, they are assumed to belong to the same entity -- This is the single most powerful heuristic in chain analysis -- Why it works: wallets automatically combine UTXOs when spending -- How Payjoin (Module 3) specifically breaks this assumption - -**Change Output Detection** -- Round amounts: sending 0.1 BTC means the other output is likely change -- Script type matching: if inputs are P2WPKH, the change is usually P2WPKH too -- Wallet-specific patterns: output ordering, fee estimation behavior -- Unnecessary input heuristic: if an input could be removed and the transaction still works, the smaller output is likely change - -**Address Reuse and Clustering** -- Reusing an address links all transactions to the same entity -- Cluster analysis: combining CIOH + address reuse to build entity graphs -- Why HD wallets (BIP32) generate fresh addresses - and why that's not enough -- How Silent Payments (Module 2) solve this at the protocol level - -**Timing Analysis and Fee Fingerprinting** -- Transaction broadcast timing can reveal geographic location -- Fee rate patterns identify wallet software -- nLockTime, nSequence, and version field differences between wallets -- Mempool observation and transaction relay timing - -### Hands-On Exercise - -**Trace a transaction chain on testnet/signet:** - -1. Go to [mempool.space/signet](https://mempool.space/signet) or [OXT.me](https://oxt.me/) -2. Find a transaction with multiple inputs -3. Apply the common-input-ownership heuristic - which inputs belong to the same wallet? -4. Identify the change output - which heuristic did you use? -5. Follow the change output forward - can you trace the entity through 3+ transactions? -6. Document your analysis: which heuristics worked? Which were ambiguous? - -### Discussion Questions - -1. If you were building a wallet, which heuristic would you prioritize defeating first? Why? -2. Chain analysis firms claim 80-90% accuracy. What does that mean for the 10-20% of misidentified users? -3. Is it possible to make chain analysis completely impossible on a transparent ledger? Why or why not? - -### Reading - -- [Bitcoin Wiki: Privacy](https://en.bitcoin.it/wiki/Privacy) (comprehensive - read Sections 1-5) -- [Bitcoin Optech: Output Linking](https://bitcoinops.org/en/topics/output-linking/) - ---- - -## Session 2: Privacy as a Protocol Property - -### Overview - -Privacy isn't a feature you bolt on - it's a property of the protocol. This session explores why privacy matters for Bitcoin's long-term viability, the difference between privacy and anonymity, and how privacy improvements at the base layer benefit all users, not just those who explicitly opt in. - -### Topics - -**Greg Maxwell's 2013 Post on Bitcoin Privacy** -- "Your inlaws don't see that you're buying birth control that deprives them of grandchildren" -- Traditional banking provides privacy by default - Bitcoin currently doesn't -- The practical consequences: theft targeting, competitive intelligence, discrimination -- Read the original: [bitcointalk.org/index.php?topic=279249.0](https://bitcointalk.org/index.php?topic=279249.0) - -**Why "I Have Nothing to Hide" Is Wrong** -- Privacy is about power asymmetry, not guilt -- Fungibility: if coins can be traced and rejected, Bitcoin loses a core monetary property -- The merchant problem: accepting bitcoin exposes your revenue, suppliers, and customers -- The donation problem: supporting a cause reveals your political beliefs to anyone who checks -- The savings problem: your wealth is visible to anyone who knows one of your addresses - -**Privacy vs Anonymity - What Bitcoin Actually Provides** -- Pseudonymous, not anonymous - your identity is one data leak away -- The privacy spectrum: - - **Transparent**: all transactions visible, identity known (KYC exchange → your address) - - **Pseudonymous**: transactions visible, identity unknown (fresh address, no KYC) - - **Private**: transactions obscured, identity unknown (CoinJoin, Payjoin, eCash) - - **Anonymous**: transactions and identity completely hidden (nearly impossible on-chain) -- Bitcoin's base layer is currently "pseudonymous" - the goal is "private by default" - -**Privacy Improvements That Help Everyone** -- Silent Payments: every payment goes to a unique address (no more address reuse) -- Payjoin: breaks CIOH even for users who don't care about privacy -- Taproot: all spends look the same regardless of script complexity -- The "anonymity set" concept: privacy tools only work when many people use them -- Why base-layer privacy > application-layer privacy - -### Discussion Questions - -1. Greg Maxwell wrote his post in 2013. What has changed since then? What hasn't? -2. If Silent Payments were enabled by default in every wallet, how would that change chain analysis? -3. Taproot makes all spends look identical. Why isn't that enough for privacy? -4. What's the difference between "privacy by default" and "privacy as an option"? Why does it matter? - -### Reading - -- [Greg Maxwell: CoinJoin: Bitcoin privacy for the real world (2013)](https://bitcointalk.org/index.php?topic=279249.0) -- [Bitcoin Optech: Taproot](https://bitcoinops.org/en/topics/taproot/) -- [OpenSats: Advancements in On-Chain Privacy](https://opensats.org/blog/developing-advancements-in-onchain-privacy) - -### Assessment - -Write a 500-word essay: **"What is the most impactful privacy improvement that could be made to Bitcoin today, and why?"** Bring this to the next session for group discussion. - ---- - -*Code Orange Dev School | [codeorange.dev](https://codeorange.dev) | CC0 1.0 Universal* diff --git a/privacy-track/module-01-chain-analysis/exercises/.gitkeep b/privacy-track/module-01-chain-analysis/exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/privacy-track/module-01-chain-analysis/exercises/chain_analysis_lab.py b/privacy-track/module-01-chain-analysis/exercises/chain_analysis_lab.py deleted file mode 100644 index 8261ab3..0000000 --- a/privacy-track/module-01-chain-analysis/exercises/chain_analysis_lab.py +++ /dev/null @@ -1,613 +0,0 @@ -""" -Bitcoin Privacy Track — Module 1: Chain Analysis Lab -Code Orange Dev School | codeorange.dev - -Hands-on chain analysis exercise: parse real Bitcoin transactions, -apply surveillance heuristics, and measure their accuracy. - -This lab simulates what chain analysis firms do — then teaches you -how to defeat it. - -Prerequisites: -- Python 3.8+ -- pip install requests (for fetching from mempool.space API) -- Understanding of Bitcoin transaction structure (inputs, outputs, scripts) - -Instructions: -- Complete each function by filling in the code where indicated -- Run: python3 chain_analysis_lab.py -- Bring your results to the weekly session -""" - -import json -import hashlib -import struct -from typing import List, Dict, Tuple, Optional - - -# ============================================================ -# Transaction Data Structures (provided) -# ============================================================ - -class TxInput: - """Represents a transaction input.""" - def __init__(self, txid: str, vout: int, script_type: str, address: str, value: int): - self.txid = txid # Previous transaction hash - self.vout = vout # Output index being spent - self.script_type = script_type # e.g., "p2wpkh", "p2tr", "p2sh-p2wpkh", "p2pkh" - self.address = address - self.value = value # Value in satoshis - - def __repr__(self): - return f"Input({self.address[:12]}..., {self.value} sats, {self.script_type})" - - -class TxOutput: - """Represents a transaction output.""" - def __init__(self, index: int, script_type: str, address: str, value: int): - self.index = index - self.script_type = script_type - self.address = address - self.value = value # Value in satoshis - - def __repr__(self): - return f"Output({self.address[:12]}..., {self.value} sats, {self.script_type})" - - -class Transaction: - """Represents a Bitcoin transaction with metadata.""" - def __init__(self, txid: str, inputs: List[TxInput], outputs: List[TxOutput], - version: int = 2, locktime: int = 0, size: int = 0, fee: int = 0): - self.txid = txid - self.inputs = inputs - self.outputs = outputs - self.version = version - self.locktime = locktime - self.size = size - self.fee = fee - - @property - def fee_rate(self) -> float: - """Fee rate in sat/vB.""" - return self.fee / self.size if self.size > 0 else 0 - - def __repr__(self): - return f"TX({self.txid[:12]}..., {len(self.inputs)} in, {len(self.outputs)} out, fee={self.fee})" - - -# ============================================================ -# Sample Transactions (simulated mainnet-like data) -# ============================================================ - -def load_sample_transactions() -> List[Transaction]: - """Load sample transactions for analysis. - - These simulate real mainnet patterns. In practice, you'd fetch - these from mempool.space or your own full node. - """ - transactions = [] - - # TX 1: Simple payment — 1 input, 2 outputs (classic change pattern) - transactions.append(Transaction( - txid="a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456", - inputs=[ - TxInput("prev_tx_01", 0, "p2wpkh", "bc1q sender_addr_001", 500_000), - ], - outputs=[ - TxOutput(0, "p2wpkh", "bc1q recipient_addr_01", 100_000), - TxOutput(1, "p2wpkh", "bc1q change_addr_00001", 399_500), - ], - version=2, locktime=840_100, size=141, fee=500 - )) - - # TX 2: Multi-input consolidation — 3 inputs, 1 output - transactions.append(Transaction( - txid="b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567a", - inputs=[ - TxInput("prev_tx_02", 0, "p2wpkh", "bc1q sender_addr_002", 50_000), - TxInput("prev_tx_03", 1, "p2wpkh", "bc1q sender_addr_003", 30_000), - TxInput("prev_tx_04", 0, "p2wpkh", "bc1q sender_addr_004", 20_000), - ], - outputs=[ - TxOutput(0, "p2wpkh", "bc1q consolidation_001", 99_500), - ], - version=2, locktime=840_101, size=250, fee=500 - )) - - # TX 3: Possible Payjoin — 2 inputs (different script types!), 2 outputs - transactions.append(Transaction( - txid="c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567ab0", - inputs=[ - TxInput("prev_tx_05", 0, "p2wpkh", "bc1q sender_addr_005", 300_000), - TxInput("prev_tx_06", 2, "p2tr", "bc1p receiver_addr_01", 200_000), - ], - outputs=[ - TxOutput(0, "p2tr", "bc1p output_addr_00001", 450_000), - TxOutput(1, "p2wpkh", "bc1q output_addr_00002", 49_500), - ], - version=2, locktime=840_102, size=210, fee=500 - )) - - # TX 4: Round amount payment — 1 input, 2 outputs - transactions.append(Transaction( - txid="d4e5f6789012345678901234567890abcdef1234567890abcdef1234567ab0c1", - inputs=[ - TxInput("prev_tx_07", 0, "p2tr", "bc1p sender_addr_006", 1_000_000), - ], - outputs=[ - TxOutput(0, "p2tr", "bc1p output_addr_00003", 500_000), # Round 0.005 BTC - TxOutput(1, "p2tr", "bc1p output_addr_00004", 499_200), # Non-round - ], - version=2, locktime=840_103, size=154, fee=800 - )) - - # TX 5: Address reuse (same output address as a previous TX) - transactions.append(Transaction( - txid="e5f6789012345678901234567890abcdef1234567890abcdef1234567ab0c1d2", - inputs=[ - TxInput("prev_tx_08", 0, "p2wpkh", "bc1q sender_addr_007", 200_000), - ], - outputs=[ - TxOutput(0, "p2wpkh", "bc1q recipient_addr_01", 150_000), # SAME as TX 1! - TxOutput(1, "p2wpkh", "bc1q change_addr_00002", 49_300), - ], - version=1, locktime=0, size=141, fee=700 # Note: version 1, locktime 0 - )) - - # TX 6: Likely CoinJoin — 3 inputs, 3 equal outputs + 2 change - transactions.append(Transaction( - txid="f6789012345678901234567890abcdef1234567890abcdef1234567ab0c1d2e3", - inputs=[ - TxInput("prev_tx_09", 0, "p2wpkh", "bc1q cj_input_001", 600_000), - TxInput("prev_tx_10", 0, "p2wpkh", "bc1q cj_input_002", 500_000), - TxInput("prev_tx_11", 0, "p2wpkh", "bc1q cj_input_003", 400_000), - ], - outputs=[ - TxOutput(0, "p2wpkh", "bc1q cj_out_001", 300_000), # Equal - TxOutput(1, "p2wpkh", "bc1q cj_out_002", 300_000), # Equal - TxOutput(2, "p2wpkh", "bc1q cj_out_003", 300_000), # Equal - TxOutput(3, "p2wpkh", "bc1q cj_change_01", 299_000), - TxOutput(4, "p2wpkh", "bc1q cj_change_02", 199_500), - ], - version=2, locktime=840_105, size=500, fee=1500 - )) - - # TX 7: Mixed script types in inputs — wallet migration? - transactions.append(Transaction( - txid="789012345678901234567890abcdef1234567890abcdef1234567ab0c1d2e3f4", - inputs=[ - TxInput("prev_tx_12", 0, "p2pkh", "1LegacyAddr0001", 100_000), - TxInput("prev_tx_13", 0, "p2sh-p2wpkh", "3WrappedSegwit01", 100_000), - TxInput("prev_tx_14", 0, "p2wpkh", "bc1q native_sw001", 100_000), - ], - outputs=[ - TxOutput(0, "p2tr", "bc1p taproot_addr01", 299_000), - ], - version=2, locktime=840_106, size=350, fee=1000 - )) - - # TX 8: Unnecessary input heuristic — one input would suffice - transactions.append(Transaction( - txid="9012345678901234567890abcdef1234567890abcdef1234567ab0c1d2e3f456", - inputs=[ - TxInput("prev_tx_15", 0, "p2wpkh", "bc1q sender_addr_010", 800_000), - TxInput("prev_tx_16", 0, "p2wpkh", "bc1q sender_addr_011", 300_000), - ], - outputs=[ - TxOutput(0, "p2wpkh", "bc1q payment_addr_001", 50_000), - TxOutput(1, "p2wpkh", "bc1q change_addr_003", 1_049_300), - ], - version=2, locktime=840_107, size=210, fee=700 - )) - - return transactions - - -# ============================================================ -# Exercise 1: Common-Input-Ownership Heuristic (CIOH) -# ============================================================ - -def apply_cioh(tx: Transaction) -> Dict[str, List[str]]: - """Apply the Common-Input-Ownership Heuristic. - - CIOH states: all inputs in a transaction are controlled by the same entity. - - Args: - tx: a Transaction object - - Returns: - dict with keys: - 'cluster': list of input addresses assumed to belong to the same entity - 'confidence': 'high', 'medium', or 'low' - 'reasoning': string explaining your confidence level - """ - # YOUR CODE HERE - # - # 1. Extract all input addresses into a list (the "cluster") - # - # 2. Determine your confidence level: - # - HIGH: all inputs have the same script type (wallet probably combined its own UTXOs) - # - MEDIUM: mixed script types but could be wallet migration - # - LOW: signs suggest this might be a CoinJoin or Payjoin (CIOH would be wrong) - # - # Hints for detecting LOW confidence: - # - If there are equal-value outputs (CoinJoin signal) - # - If inputs have very different script types AND outputs do too (Payjoin signal) - # - If the number of outputs is much larger than 2 - # - # 3. Write your reasoning string explaining why you chose that confidence level - # - # Return: {'cluster': [...], 'confidence': '...', 'reasoning': '...'} - - pass - - -# ============================================================ -# Exercise 2: Change Output Detection -# ============================================================ - -def detect_change_output(tx: Transaction) -> Dict[str, any]: - """Detect which output is the change output. - - Apply multiple heuristics and score each output. - - Args: - tx: a Transaction object - - Returns: - dict with keys: - 'likely_change_index': int (output index) or None if can't determine - 'heuristics_applied': list of strings describing each heuristic used - 'scores': dict mapping output index to a confidence score (0-100) - 'reasoning': string explaining your analysis - """ - # YOUR CODE HERE - # - # Apply these heuristics (each one adds or subtracts from the score): - # - # 1. ROUND AMOUNT HEURISTIC: - # - If one output is a round number (divisible by 100,000 sats = 0.001 BTC), - # it's likely the PAYMENT, not change. - # - The non-round output is likely change. - # Score: +30 for the non-round output being change - # - # 2. SCRIPT TYPE MATCHING: - # - Change usually matches the script type of the inputs - # - If inputs are p2wpkh and one output is p2wpkh (matching), that's likely change - # Score: +25 for the output matching input script type - # - # 3. LARGEST OUTPUT HEURISTIC: - # - In a simple payment, the larger output is often change - # (because the sender had more than they needed) - # Score: +10 for the larger output being change - # - # 4. UNNECESSARY INPUT HEURISTIC: - # - If removing any single input still covers the smaller output, - # the smaller output is likely the payment. - # Score: +20 for the smaller output being payment (larger = change) - # - # 5. ADDRESS REUSE: - # - If an output address appears in the inputs of the same or other txs, - # it's likely a payment to a known entity (not change) - # - We can't check this in isolation, but flag it if output address - # matches any input address in this tx - # - # Combine scores and return your analysis - - pass - - -# ============================================================ -# Exercise 3: Wallet Fingerprinting -# ============================================================ - -def fingerprint_wallet(tx: Transaction) -> Dict[str, str]: - """Identify likely wallet software from transaction metadata. - - Args: - tx: a Transaction object - - Returns: - dict with keys: - 'likely_wallet': string (e.g., "Bitcoin Core", "Electrum", "Mobile wallet", "Unknown") - 'fingerprints': list of strings describing each fingerprint found - 'confidence': 'high', 'medium', or 'low' - """ - # YOUR CODE HERE - # - # Check these fingerprints: - # - # 1. VERSION FIELD: - # - Version 1: older wallets (some Electrum versions, legacy software) - # - Version 2: most modern wallets (Bitcoin Core, BDK-based, etc.) - # - # 2. nLockTime: - # - 0: Electrum, many mobile wallets, some hardware wallet software - # - Current block height (> 500,000 and recent): Bitcoin Core (anti-fee-sniping) - # - Non-zero but not block height: unusual, possibly custom software - # - # 3. INPUT SCRIPT TYPES: - # - p2pkh only: very old wallet or intentionally legacy - # - p2sh-p2wpkh: transitional period wallet (2017-2020 era) - # - p2wpkh: modern SegWit wallet - # - p2tr: cutting-edge Taproot wallet (Bitcoin Core 24+, Sparrow, etc.) - # - Mixed types: wallet migration or consolidation - # - # 4. FEE RATE: - # - Round fee rate (exact multiples of 1 sat/vB): many wallets - # - Non-round: Bitcoin Core's estimatesmartfee - # - Very high or very low: possible RBF or batch transaction - # - # 5. NUMBER OF OUTPUTS: - # - Exactly 2: standard wallet payment - # - 1: consolidation or sweep - # - 3+: batched payment (exchange) or CoinJoin - # - # Combine fingerprints and return your analysis - - pass - - -# ============================================================ -# Exercise 4: CoinJoin Detection -# ============================================================ - -def detect_coinjoin(tx: Transaction) -> Dict[str, any]: - """Detect if a transaction is likely a CoinJoin. - - Args: - tx: a Transaction object - - Returns: - dict with keys: - 'is_likely_coinjoin': bool - 'equal_output_groups': list of lists (groups of equal-value output indices) - 'anonymity_set_size': int (number of equal outputs in largest group) - 'signals': list of strings describing CoinJoin signals found - """ - # YOUR CODE HERE - # - # CoinJoin signals: - # - # 1. EQUAL-VALUE OUTPUTS: - # - Group outputs by value - # - If any group has 2+ outputs with the same value, that's a strong CoinJoin signal - # - The size of the largest equal-value group = the anonymity set - # - # 2. MANY INPUTS FROM DIFFERENT ADDRESSES: - # - CoinJoins typically have 3+ inputs from different addresses - # - All addresses being unique (no address reuse) is expected - # - # 3. MORE OUTPUTS THAN INPUTS: - # - CoinJoins often have more outputs than a normal transaction - # - Equal outputs + change outputs for each participant - # - # 4. INPUT/OUTPUT RATIO: - # - Normal tx: 1-3 inputs, 1-2 outputs - # - CoinJoin: 3+ inputs, 3+ outputs (often many more) - # - # Return your analysis - - pass - - -# ============================================================ -# Exercise 5: Full Transaction Analysis Report -# ============================================================ - -def analyze_transaction(tx: Transaction) -> Dict[str, any]: - """Produce a full chain analysis report for a transaction. - - Combines all heuristics into a comprehensive analysis. - - Args: - tx: a Transaction object - - Returns: - dict with keys: - 'txid': transaction hash - 'tx_type': 'simple_payment', 'consolidation', 'coinjoin', 'payjoin', - 'batch_payment', 'sweep', or 'unknown' - 'cioh_analysis': result from apply_cioh() - 'change_detection': result from detect_change_output() - 'wallet_fingerprint': result from fingerprint_wallet() - 'coinjoin_detection': result from detect_coinjoin() - 'privacy_score': int 0-100 (100 = most private/hardest to analyze) - 'summary': string summarizing the analysis - """ - # YOUR CODE HERE - # - # 1. Run all four analysis functions above - # - # 2. Determine the transaction type: - # - 1 input, 2 outputs, one round amount → simple_payment - # - Multiple inputs, 1 output → consolidation - # - Equal outputs detected → coinjoin - # - Multiple inputs with mixed script types, 2 outputs → possible payjoin - # - 1 input, many outputs → batch_payment - # - Multiple inputs, 1 output, all from same script type → sweep - # - # 3. Calculate privacy score (0-100): - # - Start at 50 - # - Subtract 10 if change output is obvious - # - Subtract 10 if wallet fingerprint confidence is high - # - Subtract 15 if CIOH confidence is high - # - Add 20 if could be CoinJoin - # - Add 15 if could be Payjoin - # - Add 10 if all outputs are same script type (harder to distinguish) - # - Subtract 10 for address reuse - # - # 4. Write a summary paragraph - # - # Return the full analysis - - pass - - -# ============================================================ -# Exercise 6: De-anonymization Chain (Advanced) -# ============================================================ - -def trace_entity(transactions: List[Transaction], start_address: str, - max_hops: int = 5) -> List[Dict]: - """Trace an entity across multiple transactions using heuristics. - - Starting from a known address, follow the chain of transactions - to build an activity profile. - - Args: - transactions: list of Transaction objects (the "blockchain") - start_address: the address to start tracing from - max_hops: maximum number of transaction hops to follow - - Returns: - list of dicts, each representing one hop: - {'txid': str, 'role': 'sender' or 'receiver', - 'value': int, 'linked_addresses': [str]} - """ - # YOUR CODE HERE - # - # Algorithm: - # 1. Find all transactions where start_address appears (as input or output) - # 2. For each transaction: - # a. If address is in inputs → entity is SENDING - # - Apply CIOH to find other addresses owned by same entity - # - Identify the change output → that's the entity's next address - # - Add the change address to the entity's known addresses - # b. If address is in outputs → entity is RECEIVING - # - The entity now controls this UTXO - # 3. Follow the change output to the next transaction - # 4. Build the trace until max_hops or no more transactions - # - # This simulates what chain analysis firms do at scale. - # The key insight: each hop can reveal new addresses, - # and CIOH links them all together. - - pass - - -# ============================================================ -# Run all exercises -# ============================================================ - -def run_analysis(): - print("=" * 70) - print("Bitcoin Privacy Track — Chain Analysis Lab") - print("Code Orange Dev School | codeorange.dev") - print("=" * 70) - - transactions = load_sample_transactions() - - # Exercise 1: CIOH - print("\n" + "=" * 70) - print("EXERCISE 1: Common-Input-Ownership Heuristic") - print("=" * 70) - for i, tx in enumerate(transactions): - result = apply_cioh(tx) - if result is None: - print(f"\n TX {i+1}: apply_cioh() not implemented yet") - else: - print(f"\n TX {i+1} ({tx.txid[:16]}...):") - print(f" Cluster: {result['cluster']}") - print(f" Confidence: {result['confidence']}") - print(f" Reasoning: {result['reasoning']}") - - # Exercise 2: Change Detection - print("\n" + "=" * 70) - print("EXERCISE 2: Change Output Detection") - print("=" * 70) - for i, tx in enumerate(transactions): - if len(tx.outputs) < 2: - continue - result = detect_change_output(tx) - if result is None: - print(f"\n TX {i+1}: detect_change_output() not implemented yet") - else: - print(f"\n TX {i+1} ({tx.txid[:16]}...):") - print(f" Likely change: output {result['likely_change_index']}") - print(f" Heuristics: {result['heuristics_applied']}") - print(f" Scores: {result['scores']}") - - # Exercise 3: Wallet Fingerprinting - print("\n" + "=" * 70) - print("EXERCISE 3: Wallet Fingerprinting") - print("=" * 70) - for i, tx in enumerate(transactions): - result = fingerprint_wallet(tx) - if result is None: - print(f"\n TX {i+1}: fingerprint_wallet() not implemented yet") - else: - print(f"\n TX {i+1}:") - print(f" Likely wallet: {result['likely_wallet']}") - print(f" Fingerprints: {result['fingerprints']}") - print(f" Confidence: {result['confidence']}") - - # Exercise 4: CoinJoin Detection - print("\n" + "=" * 70) - print("EXERCISE 4: CoinJoin Detection") - print("=" * 70) - for i, tx in enumerate(transactions): - result = detect_coinjoin(tx) - if result is None: - print(f"\n TX {i+1}: detect_coinjoin() not implemented yet") - else: - print(f"\n TX {i+1}:") - print(f" CoinJoin? {result['is_likely_coinjoin']}") - if result['is_likely_coinjoin']: - print(f" Anonymity set: {result['anonymity_set_size']}") - print(f" Signals: {result['signals']}") - - # Exercise 5: Full Analysis - print("\n" + "=" * 70) - print("EXERCISE 5: Full Transaction Analysis") - print("=" * 70) - for i, tx in enumerate(transactions): - result = analyze_transaction(tx) - if result is None: - print(f"\n TX {i+1}: analyze_transaction() not implemented yet") - else: - print(f"\n TX {i+1} ({tx.txid[:16]}...):") - print(f" Type: {result['tx_type']}") - print(f" Privacy score: {result['privacy_score']}/100") - print(f" Summary: {result['summary']}") - - # Exercise 6: Entity Tracing - print("\n" + "=" * 70) - print("EXERCISE 6: Entity Tracing") - print("=" * 70) - trace = trace_entity(transactions, "bc1q recipient_addr_01", max_hops=3) - if trace is None: - print("\n trace_entity() not implemented yet") - else: - print(f"\n Tracing entity from bc1q recipient_addr_01:") - for hop in trace: - print(f" Hop: {hop['txid'][:16]}... | Role: {hop['role']} | " - f"Value: {hop['value']} sats | Linked: {hop['linked_addresses']}") - - print("\n" + "=" * 70) - print("REFLECTION QUESTIONS (write 2-3 sentences each):") - print("=" * 70) - print(""" - 1. Which heuristic was most reliable across the sample transactions? - Which was most often wrong? - - 2. TX 3 might be a Payjoin. How does this affect every other heuristic - you applied to that transaction? - - 3. TX 5 reuses an address from TX 1. What does this single data point - reveal about the entity behind bc1q recipient_addr_01? - - 4. TX 6 looks like a CoinJoin. If you were a chain analysis firm, - how would you try to unmix it? What additional data would you need? - - 5. If you could change ONE thing about Bitcoin's transaction format - to defeat chain analysis, what would it be? - """) - - print("=" * 70) - print("Done! Bring your code and reflection answers to the weekly call.") - print("=" * 70) - - -if __name__ == "__main__": - run_analysis() diff --git a/privacy-track/module-02-silent-payments/README.md b/privacy-track/module-02-silent-payments/README.md deleted file mode 100644 index cd8d97e..0000000 --- a/privacy-track/module-02-silent-payments/README.md +++ /dev/null @@ -1,197 +0,0 @@ -# Module 2: Silent Payments - BIP352 - -> 4 sessions - Understand, implement, and contribute to Bitcoin's reusable address protocol. - ---- - -## Session 1: How Silent Payments Work - -### Overview - -Address reuse is one of Bitcoin's biggest privacy failures. Every time you post a donation address, every payment to it is linked. Silent Payments (BIP352) solve this by allowing a single static address to generate a unique on-chain address for every payment - without any interaction between sender and receiver. - -### Topics - -**The Problem: Address Reuse Destroys Privacy** -- Posting a Bitcoin address publicly links all payments to your identity -- Even "fresh address per payment" requires interaction (invoice model) -- The donation/tip use case: you need one static address, but reuse destroys privacy -- BIP47 reusable payment codes - the first attempt (requires a notification transaction) - -**Static Payment Codes vs Fresh Addresses** -- Traditional: receiver generates a new address for each payment (requires online interaction) -- BIP47: sender sends a notification transaction first, then derives addresses (extra on-chain cost) -- BIP352 Silent Payments: sender derives a unique address from receiver's static code + sender's inputs (no notification, no interaction) - -**ECDH Key Exchange for Payment Detection** -- Elliptic Curve Diffie-Hellman: two parties derive a shared secret without prior communication -- The sender uses their private key + receiver's public key → shared secret -- The receiver uses their private key + sender's public key → same shared secret -- This shared secret tweaks the receiver's spend key to produce a unique output address - -**Scan Keys vs Spend Keys** -- Silent Payment address encodes two public keys: B_scan and B_spend -- B_scan: used by the receiver to detect incoming payments (can be on a hot/online device) -- B_spend: used to actually spend the received coins (kept cold/offline) -- This separation allows watch-only detection without exposing spending capability - -**Walk Through the BIP352 Specification** -- Address format: `sp1q...` (bech32m encoded) -- Sending algorithm: sum input private keys → ECDH with B_scan → tweak B_spend -- Receiving algorithm: for each transaction, compute ECDH with summed input public keys → check outputs -- Label support: receiver can have multiple "sub-addresses" from one Silent Payment address - -### Discussion Questions - -1. Why doesn't BIP352 require a notification transaction like BIP47 does? -2. What is the privacy trade-off of the scan key being on a hot device? -3. If Silent Payments were in every wallet, how would the anonymity set change? -4. What are the computational costs of scanning? Why is this a challenge for light clients? - -### Reading - -- [BIP352 Specification](https://bips.dev/352/) -- [Bitcoin Optech: Silent Payments](https://bitcoinops.org/en/topics/silent-payments/) -- [How Silent Payments Work (Otto)](https://medium.com/@ottosch/how-silent-payments-work-41bea907d6b0) - ---- - -## Session 2: Implement a Minimal Silent Payments Sender (Python) - -### Overview - -You'll implement a simplified Silent Payments sender in Python. By the end of this session, you'll have code that takes a Silent Payment address and constructs a transaction with a unique output that only the receiver can detect. - -### Topics - -**Generate a Silent Payment Address** -- Derive B_scan and B_spend from a seed/xpub at path m/352h/0h/0h -- Encode as a bech32m `sp1q...` address -- Parse the address to extract the two public keys - -**Derive the Shared Secret from Sender Inputs** -- Sum all input private keys (or use the input tweak for Taproot) -- Compute `input_hash = hash(outpoint || A_sum)` for deterministic tweaking -- ECDH: `shared_secret = input_hash * a_sum * B_scan` -- This ensures each transaction produces a different shared secret - -**Calculate the Tweak and Output Key** -- `t_k = hash(shared_secret || k)` where k is the output index -- `output_key = B_spend + t_k * G` -- The output is a standard P2TR (Taproot) output paying to `output_key` - -**Construct the Transaction** -- Build a transaction with your inputs and the Silent Payment output -- The output looks like any other Taproot output on-chain - no special markers -- Indistinguishable from normal Taproot transactions - -### Exercise - -See [`exercises/silent_payments_sender.py`](exercises/silent_payments_sender.py) - -**Your Task:** -1. Implement the `generate_silent_payment_address()` function -2. Implement the `derive_shared_secret()` function -3. Implement the `compute_output_key()` function -4. Send a Silent Payment on signet using your implementation -5. Verify the output using the receiver's scan key - -### Discussion Questions - -1. Why do we sum all input private keys rather than using just one? -2. What happens if the sender has only Taproot inputs vs only SegWit inputs? -3. How does the `input_hash` prevent the same shared secret across transactions? - ---- - -## Session 3: Scanning and Receiving Silent Payments - -### Overview - -Sending is the easy part. Receiving Silent Payments requires scanning every transaction in every block to check if any outputs belong to you. This session covers the computational challenge and the solutions being developed. - -### Topics - -**The Computational Cost of Scanning** -- For every transaction, the receiver must: extract input public keys → sum them → ECDH with scan key → check each output -- On mainnet: ~300,000 transactions per day = 300,000 ECDH operations -- This is orders of magnitude more expensive than standard address matching -- Why this makes Silent Payments challenging for mobile/light wallets - -**Compact Block Filters (BIP157/158) for Efficient Scanning** -- Instead of downloading full blocks, download a compact filter per block -- Filter tells you "this block might contain a transaction for you" (probabilistic) -- Only download the full block when the filter matches -- Dramatically reduces bandwidth and computation for light clients - -**Light Client Considerations** -- Kyoto: Rust implementation of BIP157/158 compact block filter client -- Neutrino: Go implementation used by LND -- The trade-off: privacy (compact filters) vs efficiency (trusted server) -- Hybrid approaches: use a trusted server for scanning, verify with your own node - -### Exercise - -See [`exercises/silent_payments_scanner.py`](exercises/silent_payments_scanner.py) - -**Your Task:** -1. Implement `scan_transaction()` - check if a transaction contains a Silent Payment to you -2. Implement `scan_block()` - scan all transactions in a block -3. Benchmark: how long does it take to scan 100 blocks on signet? -4. Implement a basic compact block filter check to skip irrelevant blocks -5. Compare scanning time with and without the filter optimization - -### Discussion Questions - -1. Why can't Silent Payments use the same simple UTXO scanning as BIP32 wallets? -2. What is an acceptable scanning time for a mobile wallet? How far are we from that? -3. Could a third-party scanning service help without compromising privacy? How? - ---- - -## Session 4: Contributing to Silent Payments - -### Overview - -You've implemented Silent Payments from scratch. Now it's time to contribute to the real implementations. This session walks through the active codebases and helps you find your first contribution. - -### Topics - -**Bitcoin Core PR #28122 - Code Walkthrough** -- Where Silent Payments lives in the Bitcoin Core codebase -- The `Sender` class and `GenerateRecipientScriptPubKeys()` method -- The `Recipient` class and `ScanTxOutputs()` method -- Test vectors and how to run the test suite -- Current status and open review comments - -**rust-silentpayments Library** -- [cygnet3/rust-silentpayments](https://github.com/cygnet3/rust-silentpayments) - standalone Rust library -- API walkthrough: creating addresses, sending, scanning -- How it integrates with rust-bitcoin and BDK -- Open issues and areas needing contribution - -**Wallet Integration Status** -- Which wallets are implementing Silent Payments (as of 2026) -- The UX challenges: scanning time, first-use experience, backup/recovery -- What needs to happen for mainstream adoption -- Where your contribution can have the most impact - -### Exercise - -**Your First PR:** -1. Pick one of these repositories: - - Bitcoin Core (`bitcoin/bitcoin`) - C++ - - rust-silentpayments (`cygnet3/rust-silentpayments`) - Rust - - Any wallet implementing BIP352 -2. Find an open issue labeled "good first issue" or identify an improvement from the code walkthrough -3. Fork the repo, make your change, write tests -4. Submit your PR with a clear description of what you changed and why -5. Respond to review comments - -### Assessment - -**Deliverable**: A submitted (not necessarily merged) PR to a Silent Payments implementation. Bring the PR link to the next session for group code review. - ---- - -*Code Orange Dev School | [codeorange.dev](https://codeorange.dev) | CC0 1.0 Universal* diff --git a/privacy-track/module-02-silent-payments/exercises/silent_payments_scanner.py b/privacy-track/module-02-silent-payments/exercises/silent_payments_scanner.py deleted file mode 100644 index b8c7b50..0000000 --- a/privacy-track/module-02-silent-payments/exercises/silent_payments_scanner.py +++ /dev/null @@ -1,236 +0,0 @@ -""" -Bitcoin Privacy Track — Module 2, Session 3: Silent Payments Scanner -Code Orange Dev School | codeorange.dev - -Implement a minimal Silent Payments receiver/scanner. - -The challenge: to detect a Silent Payment, the receiver must scan -every transaction in every block and perform an ECDH computation -to check if any output belongs to them. - -Prerequisites: -- Completed silent_payments_sender.py (Session 2) -- Understanding of ECDH and BIP352 key derivation - -Instructions: -- Complete each function by filling in the code where indicated -- Run this file with: python3 silent_payments_scanner.py -- Discuss your solutions at the weekly call -""" - -import hashlib -import secrets -import time - -# Import helpers from the sender exercise -# (In practice, you'd have these in a shared module) -from silent_payments_sender import ( - sha256, tagged_hash, P, N, G, Gx, Gy, - point_add, scalar_mult, privkey_to_pubkey, - serialize_pubkey, generate_silent_payment_keypair, - derive_shared_secret, compute_output_key -) - - -# ============================================================ -# Exercise 1: Scan a Single Transaction -# ============================================================ -# -# Given a transaction's input public keys and outputs, -# check if any output is a Silent Payment to the receiver. - -def scan_transaction(tx_input_pubkeys: list, tx_outpoints: list, - tx_output_keys: list, b_scan: int, B_spend: tuple) -> list: - """Scan a transaction for Silent Payments to the receiver. - - Args: - tx_input_pubkeys: list of public key points [(x,y), ...] from tx inputs - tx_outpoints: list of outpoint bytes from tx inputs - tx_output_keys: list of output public key points [(x,y), ...] (Taproot outputs) - b_scan: receiver's scan private key (int) - B_spend: receiver's spend public key (x, y) - - Returns: - list of tuples: [(output_index, output_key), ...] for detected payments - """ - # YOUR CODE HERE - # - # 1. Sum all input public keys: A_sum = P1 + P2 + ... - # (Use point_add repeatedly) - # - # 2. Find the smallest outpoint (lexicographic sort) - # - # 3. Compute input_hash = tagged_hash("BIP0352/Inputs", - # smallest_outpoint + serialize_pubkey(A_sum)) - # input_hash_int = int.from_bytes(input_hash, 'big') % N - # - # 4. Compute the tweaked sum: tweaked_A = input_hash_int * A_sum - # - # 5. Receiver's ECDH: shared_secret = b_scan * tweaked_A - # - # 6. For each output index k, compute: - # expected_output = compute_output_key(shared_secret, B_spend, k) - # If expected_output matches tx_output_keys[k], it's a payment to us! - # - # 7. Return list of (output_index, output_key) for matches - - pass - - -# ============================================================ -# Exercise 2: Scan a Block -# ============================================================ -# -# Scan all transactions in a simulated block. - -def scan_block(block_txs: list, b_scan: int, B_spend: tuple) -> list: - """Scan all transactions in a block for Silent Payments. - - Args: - block_txs: list of dicts, each with keys: - 'input_pubkeys': list of (x,y) tuples - 'outpoints': list of bytes - 'output_keys': list of (x,y) tuples - b_scan: receiver's scan private key - B_spend: receiver's spend public key - - Returns: - list of tuples: [(tx_index, output_index, output_key), ...] - """ - # YOUR CODE HERE - # - # For each transaction in block_txs: - # results = scan_transaction(tx['input_pubkeys'], tx['outpoints'], - # tx['output_keys'], b_scan, B_spend) - # For each match, append (tx_index, output_index, output_key) - # - # Return all matches - - pass - - -# ============================================================ -# Exercise 3: Benchmark Scanning Performance -# ============================================================ -# -# How fast can you scan? This exercise generates simulated blocks -# and measures scanning time. - -def benchmark_scanning(num_blocks: int = 10, txs_per_block: int = 50): - """Benchmark Silent Payment scanning performance. - - Generates simulated blocks with random transactions, plants - one Silent Payment in a random block, and measures how long - it takes to find it. - """ - print(f"\nBenchmark: scanning {num_blocks} blocks x {txs_per_block} txs") - - # Generate receiver keys - b_scan, B_scan, b_spend, B_spend = generate_silent_payment_keypair() - - # Generate simulated blocks - blocks = [] - planted_block = secrets.randbelow(num_blocks) - planted_tx = secrets.randbelow(txs_per_block) - - for block_idx in range(num_blocks): - txs = [] - for tx_idx in range(txs_per_block): - # Random sender keys - num_inputs = secrets.randbelow(3) + 1 - sender_keys = [secrets.randbelow(N - 1) + 1 for _ in range(num_inputs)] - input_pubkeys = [privkey_to_pubkey(k) for k in sender_keys] - outpoints = [secrets.token_bytes(36) for _ in range(num_inputs)] - - # Random outputs (2-4 per tx) - num_outputs = secrets.randbelow(3) + 2 - output_keys = [privkey_to_pubkey(secrets.randbelow(N - 1) + 1) - for _ in range(num_outputs)] - - # Plant a real Silent Payment in one specific transaction - if block_idx == planted_block and tx_idx == planted_tx: - shared_secret = derive_shared_secret(sender_keys, outpoints, B_scan) - real_output = compute_output_key(shared_secret, B_spend, k=0) - output_keys[0] = real_output - print(f" Planted SP in block {block_idx}, tx {tx_idx}, output 0") - - txs.append({ - 'input_pubkeys': input_pubkeys, - 'outpoints': outpoints, - 'output_keys': output_keys - }) - blocks.append(txs) - - # YOUR CODE HERE - # Time the scanning process - # - # start = time.time() - # found = [] - # for block_idx, block_txs in enumerate(blocks): - # matches = scan_block(block_txs, b_scan, B_spend) - # for tx_idx, out_idx, out_key in matches: - # found.append((block_idx, tx_idx, out_idx)) - # elapsed = time.time() - start - # - # print(f"\n Scanned {num_blocks * txs_per_block} transactions in {elapsed:.3f}s") - # print(f" Rate: {num_blocks * txs_per_block / elapsed:.0f} tx/s") - # print(f" Found {len(found)} Silent Payment(s): {found}") - # - # # Mainnet has ~300,000 tx/day. How long would that take? - # mainnet_estimate = 300_000 / (num_blocks * txs_per_block / elapsed) - # print(f"\n Estimated mainnet scanning time: {mainnet_estimate:.0f}s ({mainnet_estimate/60:.1f} min) per day of blocks") - # - # QUESTION: Is this fast enough for a mobile wallet? - # QUESTION: How would compact block filters (BIP157/158) help? - - pass - - -# ============================================================ -# Run all exercises -# ============================================================ - -if __name__ == "__main__": - print("=" * 60) - print("Bitcoin Privacy Track — Silent Payments Scanner") - print("=" * 60) - - # Test scanning with a known payment - print("\nExercise 1-2: Scan for a planted Silent Payment") - - b_scan, B_scan, b_spend, B_spend = generate_silent_payment_keypair() - - if b_scan is None: - print(" Complete silent_payments_sender.py first!") - else: - # Create a transaction with a Silent Payment - sender_key = secrets.randbelow(N - 1) + 1 - outpoint = secrets.token_bytes(36) - - shared_secret = derive_shared_secret([sender_key], [outpoint], B_scan) - sp_output = compute_output_key(shared_secret, B_spend, k=0) - random_output = privkey_to_pubkey(secrets.randbelow(N - 1) + 1) - - if shared_secret is not None and sp_output is not None: - found = scan_transaction( - tx_input_pubkeys=[privkey_to_pubkey(sender_key)], - tx_outpoints=[outpoint], - tx_output_keys=[sp_output, random_output], - b_scan=b_scan, - B_spend=B_spend - ) - - if found is None: - print(" scan_transaction() not implemented yet") - elif len(found) == 1 and found[0][0] == 0: - print(f" PASSED — detected Silent Payment at output index {found[0][0]}") - else: - print(f" FAILED — expected 1 match at index 0, got: {found}") - - # Benchmark - print("\nExercise 3: Benchmark scanning performance") - benchmark_scanning(num_blocks=5, txs_per_block=20) - - print("\n" + "=" * 60) - print("Done! Bring your solutions and benchmark results to the weekly call.") - print("=" * 60) diff --git a/privacy-track/module-02-silent-payments/exercises/silent_payments_sender.py b/privacy-track/module-02-silent-payments/exercises/silent_payments_sender.py deleted file mode 100644 index d42c806..0000000 --- a/privacy-track/module-02-silent-payments/exercises/silent_payments_sender.py +++ /dev/null @@ -1,342 +0,0 @@ -""" -Bitcoin Privacy Track — Module 2, Session 2: Silent Payments Sender -Code Orange Dev School | codeorange.dev - -Implement a minimal Silent Payments (BIP352) sender in Python. - -Prerequisites: -- Understanding of elliptic curve cryptography (Bitcoin Dojo Week 1-2) -- Python 3.8+ -- pip install secp256k1 (or use the pure-Python fallback below) - -References: -- BIP352: https://bips.dev/352/ -- Bitcoin Optech: https://bitcoinops.org/en/topics/silent-payments/ - -Instructions: -- Complete each function by filling in the code where indicated -- Run this file with: python3 silent_payments_sender.py -- Discuss your solutions at the weekly call -""" - -import hashlib -import secrets - - -# ============================================================ -# Helpers (provided) -# ============================================================ - -def sha256(data: bytes) -> bytes: - return hashlib.sha256(data).digest() - - -def tagged_hash(tag: str, data: bytes) -> bytes: - """BIP340-style tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data)""" - tag_hash = sha256(tag.encode()) - return sha256(tag_hash + tag_hash + data) - - -# Secp256k1 curve parameters -P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F -N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 -Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 - - -def point_add(p1, p2): - """Add two points on secp256k1. None represents the point at infinity.""" - if p1 is None: - return p2 - if p2 is None: - return p1 - - x1, y1 = p1 - x2, y2 = p2 - - if x1 == x2 and y1 != y2: - return None - - if x1 == x2: - s = (3 * x1 * x1 * pow(2 * y1, P - 2, P)) % P - else: - s = ((y2 - y1) * pow(x2 - x1, P - 2, P)) % P - - x3 = (s * s - x1 - x2) % P - y3 = (s * (x1 - x3) - y1) % P - return (x3, y3) - - -def scalar_mult(k, point): - """Multiply a point by a scalar on secp256k1.""" - result = None - addend = point - while k: - if k & 1: - result = point_add(result, addend) - addend = point_add(addend, addend) - k >>= 1 - return result - - -G = (Gx, Gy) - - -def privkey_to_pubkey(privkey: int) -> tuple: - """Derive the public key (point) from a private key (scalar).""" - return scalar_mult(privkey, G) - - -def serialize_pubkey(point: tuple, compressed: bool = True) -> bytes: - """Serialize a public key point to bytes.""" - x, y = point - if compressed: - prefix = b'\x02' if y % 2 == 0 else b'\x03' - return prefix + x.to_bytes(32, 'big') - return b'\x04' + x.to_bytes(32, 'big') + y.to_bytes(32, 'big') - - -# ============================================================ -# Exercise 1: Generate a Silent Payment Address -# ============================================================ -# -# A Silent Payment address contains two public keys: -# B_scan — used to detect payments (can be on a hot device) -# B_spend — used to spend received coins (kept cold) -# -# Both are derived from a master key at BIP352 derivation paths. -# For this exercise, we generate them directly from random scalars. - -def generate_silent_payment_keypair(): - """Generate a Silent Payment scan/spend keypair. - - Returns: - tuple: (b_scan_privkey, b_scan_pubkey, b_spend_privkey, b_spend_pubkey) - where privkeys are ints and pubkeys are (x, y) tuples - """ - # YOUR CODE HERE - # 1. Generate a random private key for scanning (b_scan) - # b_scan = random integer in range [1, N-1] - # 2. Derive B_scan = b_scan * G - # 3. Generate a random private key for spending (b_spend) - # 4. Derive B_spend = b_spend * G - # 5. Return (b_scan, B_scan, b_spend, B_spend) - - pass - - -# ============================================================ -# Exercise 2: Derive the Shared Secret -# ============================================================ -# -# When sending a Silent Payment, the sender computes a shared -# secret using ECDH between their input keys and the receiver's -# scan public key. -# -# The process: -# 1. Sum all input private keys: a_sum = a_1 + a_2 + ... (mod N) -# 2. Compute input_hash = hash(smallest_outpoint || A_sum) -# 3. Shared secret = input_hash * a_sum * B_scan - -def derive_shared_secret(input_privkeys: list, outpoints: list, B_scan: tuple) -> tuple: - """Derive the ECDH shared secret for a Silent Payment. - - Args: - input_privkeys: list of private keys (ints) for the sender's inputs - outpoints: list of outpoint bytes (txid + vout) for ordering - B_scan: receiver's scan public key (x, y) - - Returns: - tuple: the shared secret point (x, y) - """ - # YOUR CODE HERE - # 1. Sum all input private keys modulo N - # a_sum = sum(input_privkeys) % N - # - # 2. Compute A_sum = a_sum * G (the summed public key) - # - # 3. Sort outpoints lexicographically, take the smallest - # smallest_outpoint = sorted(outpoints)[0] - # - # 4. Compute input_hash = tagged_hash("BIP0352/Inputs", - # smallest_outpoint + serialize_pubkey(A_sum)) - # Convert to integer: input_hash_int = int.from_bytes(input_hash, 'big') % N - # - # 5. Compute the shared secret point: - # shared_secret = scalar_mult((input_hash_int * a_sum) % N, B_scan) - # - # 6. Return shared_secret - - pass - - -# ============================================================ -# Exercise 3: Compute the Output Key -# ============================================================ -# -# The output key is what appears on-chain as a Taproot output. -# It's derived from the shared secret and the receiver's spend key. -# -# output_key = B_spend + t_k * G -# where t_k = tagged_hash("BIP0352/SharedSecret", ser(shared_secret) || k) - -def compute_output_key(shared_secret: tuple, B_spend: tuple, k: int = 0) -> tuple: - """Compute the Silent Payment output key. - - Args: - shared_secret: the ECDH shared secret point (x, y) - B_spend: receiver's spend public key (x, y) - k: output index (0 for first output, 1 for second, etc.) - - Returns: - tuple: the output public key (x, y) for the Taproot output - """ - # YOUR CODE HERE - # 1. Serialize the shared secret's x-coordinate (32 bytes, big-endian) - # ss_bytes = shared_secret[0].to_bytes(32, 'big') - # - # 2. Compute the tweak: - # t_k = tagged_hash("BIP0352/SharedSecret", ss_bytes + k.to_bytes(4, 'big')) - # t_k_int = int.from_bytes(t_k, 'big') % N - # - # 3. Compute the tweak point: T = t_k_int * G - # - # 4. Add to the spend key: output_key = point_add(B_spend, T) - # - # 5. Return output_key - - pass - - -# ============================================================ -# Exercise 4: Full Silent Payment Send -# ============================================================ -# -# Combine exercises 1-3 to simulate a full Silent Payment. - -def send_silent_payment(): - """Simulate sending a Silent Payment. - - 1. Receiver generates a Silent Payment address (scan + spend keys) - 2. Sender has some UTXOs with known private keys - 3. Sender computes the output key - 4. The on-chain output is a P2TR output paying to the output key - 5. Receiver can detect this payment using their scan key - """ - print("=" * 60) - print("Silent Payment Sender — Full Simulation") - print("=" * 60) - - # YOUR CODE HERE - # - # Step 1: Receiver generates their Silent Payment keypair - # result = generate_silent_payment_keypair() - # b_scan, B_scan, b_spend, B_spend = result - # print(f"\nReceiver's scan pubkey: {serialize_pubkey(B_scan).hex()}") - # print(f"Receiver's spend pubkey: {serialize_pubkey(B_spend).hex()}") - # - # Step 2: Sender has 2 inputs (simulated) - # sender_key_1 = secrets.randbelow(N - 1) + 1 - # sender_key_2 = secrets.randbelow(N - 1) + 1 - # outpoint_1 = secrets.token_bytes(36) # simulated txid:vout - # outpoint_2 = secrets.token_bytes(36) - # - # Step 3: Sender derives the shared secret - # shared_secret = derive_shared_secret( - # [sender_key_1, sender_key_2], - # [outpoint_1, outpoint_2], - # B_scan - # ) - # print(f"\nShared secret x: {shared_secret[0]}") - # - # Step 4: Sender computes the output key - # output_key = compute_output_key(shared_secret, B_spend, k=0) - # print(f"\nOutput key (on-chain Taproot address):") - # print(f" x: {output_key[0]}") - # print(f" Serialized: {serialize_pubkey(output_key).hex()}") - # - # Step 5: Verify — receiver can derive the same output key - # (Receiver uses b_scan instead of a_sum to get the same shared secret) - # a_sum = (sender_key_1 + sender_key_2) % N - # A_sum = privkey_to_pubkey(a_sum) - # smallest_outpoint = sorted([outpoint_1, outpoint_2])[0] - # input_hash = tagged_hash("BIP0352/Inputs", - # smallest_outpoint + serialize_pubkey(A_sum)) - # input_hash_int = int.from_bytes(input_hash, 'big') % N - # - # # Receiver's ECDH: b_scan * (input_hash * A_sum) - # tweaked_A = scalar_mult(input_hash_int, A_sum) - # receiver_shared_secret = scalar_mult(b_scan, tweaked_A) - # - # assert receiver_shared_secret == shared_secret, "Shared secrets don't match!" - # print("\n✓ Receiver derived the same shared secret") - # - # receiver_output_key = compute_output_key(receiver_shared_secret, B_spend, k=0) - # assert receiver_output_key == output_key, "Output keys don't match!" - # print("✓ Receiver derived the same output key — payment detected!") - # - # print(f"\n{'=' * 60}") - # print("On-chain, this looks like a normal Taproot output.") - # print("Only the receiver (with b_scan) can detect it's theirs.") - # print(f"{'=' * 60}") - - pass - - -# ============================================================ -# TESTS -# ============================================================ - -def run_tests(): - print("=" * 60) - print("Bitcoin Privacy Track — Silent Payments Sender Tests") - print("=" * 60) - - # Test 1: Keypair generation - print("\nTest 1: Keypair generation") - result = generate_silent_payment_keypair() - if result is None: - print(" SKIPPED — generate_silent_payment_keypair() not implemented yet") - else: - b_scan, B_scan, b_spend, B_spend = result - assert B_scan == privkey_to_pubkey(b_scan), "B_scan doesn't match b_scan" - assert B_spend == privkey_to_pubkey(b_spend), "B_spend doesn't match b_spend" - print(" PASSED") - - # Test 2: Shared secret derivation - print("\nTest 2: Shared secret derivation") - if result is None: - print(" SKIPPED — depends on Exercise 1") - else: - test_privkeys = [secrets.randbelow(N - 1) + 1, secrets.randbelow(N - 1) + 1] - test_outpoints = [secrets.token_bytes(36), secrets.token_bytes(36)] - ss = derive_shared_secret(test_privkeys, test_outpoints, B_scan) - if ss is None: - print(" SKIPPED — derive_shared_secret() not implemented yet") - else: - assert ss[0] > 0 and ss[1] > 0, "Shared secret should be a valid point" - print(" PASSED") - - # Test 3: Output key computation - print("\nTest 3: Output key computation") - if result is None or ss is None: - print(" SKIPPED — depends on Exercises 1-2") - else: - ok = compute_output_key(ss, B_spend, k=0) - if ok is None: - print(" SKIPPED — compute_output_key() not implemented yet") - else: - assert ok[0] > 0 and ok[1] > 0, "Output key should be a valid point" - print(" PASSED") - - # Test 4: Full simulation - print("\nTest 4: Full Silent Payment simulation") - send_silent_payment() - - print("\n" + "=" * 60) - print("Done! Bring your solutions to the weekly call.") - print("=" * 60) - - -if __name__ == "__main__": - run_tests() diff --git a/privacy-track/module-03-payjoin/README.md b/privacy-track/module-03-payjoin/README.md deleted file mode 100644 index d299a5e..0000000 --- a/privacy-track/module-03-payjoin/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Module 3: Payjoin - BIP77/78 - -> 3 sessions - Break the most powerful chain analysis heuristic, build with Payjoin Dev Kit, and contribute to payjoin-rust. - ---- - -## Session 1: How Payjoin Breaks Chain Analysis - -### Overview - -The common-input-ownership heuristic is the backbone of chain analysis. Payjoin shatters it. In a Payjoin transaction, both sender and receiver contribute inputs - making it impossible to assume all inputs belong to one entity. This session covers how Payjoin works, its evolution from V1 to V2, and why it's one of the highest-leverage privacy improvements available today. - -### Topics - -**The Common-Input-Ownership Heuristic** -- Why it exists: wallets combine UTXOs automatically when spending -- How chain analysis firms use it: "all inputs in a transaction belong to the same wallet" -- Accuracy: extremely high for normal transactions - the foundation of clustering -- The weakness: it's an assumption, not a rule. Payjoin exploits this. - -**Payjoin V1 (BIP78) - Synchronous** -- Original proposal: sender creates a PSBT, sends it to receiver's endpoint -- Receiver adds their own input(s) and adjusts outputs -- Sender verifies, signs, and broadcasts -- Limitation: receiver must run an always-on server (the `pj=` endpoint in BIP21 URI) -- This made V1 impractical for most users - -**Payjoin V2 (BIP77) - Asynchronous** -- The breakthrough: outsource receiver's server to an untrusted "Payjoin Directory" -- Receiver posts an encrypted request to the directory -- Sender picks it up, adds their contribution, posts back -- Neither party needs to be online simultaneously -- The directory learns nothing (encrypted payloads) -- Bull Bitcoin: first production wallet to ship BIP77 - -**Walk Through a Payjoin Transaction Step by Step** -1. Receiver generates a BIP21 URI with `pj=` parameter pointing to their Payjoin endpoint -2. Sender constructs a standard transaction (the "original PSBT") -3. Sender sends the PSBT to the receiver's endpoint -4. Receiver adds one or more of their own inputs to the transaction -5. Receiver adjusts the outputs (receiver's output increases by the value of their added input) -6. Receiver signs their inputs and returns the modified PSBT -7. Sender verifies: their output values haven't decreased, no new outputs were added -8. Sender signs their inputs and broadcasts the final transaction -9. On-chain: looks like a normal multi-input transaction - but CIOH is broken - -### Hands-On Exercise - -**Analyze a Payjoin transaction:** - -Given this transaction: -``` -Inputs: - - 0.5 BTC (address A) - - 0.3 BTC (address B) -Outputs: - - 0.7 BTC (address C) - - 0.1 BTC (address D) -``` - -Questions: -1. If this is a normal transaction, who owns what? Apply CIOH. -2. If this is a Payjoin, what are the possible ownership combinations? -3. How many different interpretations exist? (This is the "ambiguity set") -4. Can a chain analysis firm tell if this is a Payjoin? Why or why not? - -### Discussion Questions - -1. Payjoin V1 required the receiver to run a server. Why did that kill adoption? -2. How does Payjoin V2's directory model solve the liveness problem without compromising privacy? -3. If every merchant used Payjoin for payments, what would happen to the CIOH across the entire blockchain? -4. Payjoin increases the receiver's UTXO set. Is this a feature or a bug? - -### Reading - -- [BIP78: Payjoin V1](https://bips.dev/78/) -- [BIP77: Payjoin V2](https://github.com/bitcoin/bips/blob/master/bip-0077.mediawiki) -- [Bitcoin Optech: Payjoin](https://bitcoinops.org/en/topics/payjoin/) -- [The Payjoin Experience (Bitcoin Design)](https://bitcoin.design/guide/case-studies/payjoin/) -- [Bull Bitcoin BIP77 announcement](https://www.bullbitcoin.com/blog/bull-bitcoin-wallet-payjoin) - ---- - -## Session 2: Building with Payjoin Dev Kit - -### Overview - -Payjoin Dev Kit (PDK) is a Rust library that makes it straightforward to integrate Payjoin into any wallet. This session walks through the PDK API and has you send your first Payjoin transaction on testnet. - -### Topics - -**Payjoin Dev Kit (PDK) - Rust Library Overview** -- Repository: [payjoin/rust-payjoin](https://github.com/payjoin/rust-payjoin) -- Architecture: sender module, receiver module, directory client -- Key types: `Sender`, `Receiver`, `Request`, `Response` -- How PDK handles the BIP77 asynchronous flow - -**Integrating Payjoin into a Wallet (Sender Side)** -- Detect `pj=` parameter in BIP21 payment URI -- Construct the original PSBT using wallet's normal coin selection -- Send the PSBT to the Payjoin endpoint via PDK -- Receive the modified PSBT, verify it, sign, broadcast -- Error handling: what if the receiver is offline? What if verification fails? - -**Integrating Payjoin into a Receiver (Merchant/BTCPay)** -- Set up a Payjoin endpoint (V1: HTTP server, V2: directory) -- Receive the sender's PSBT -- Select own input(s) to contribute (UTXO consolidation opportunity) -- Modify outputs, sign, return the modified PSBT -- BTCPay Server's implementation as a reference - -### Exercise - -See [`exercises/payjoin_testnet.md`](exercises/payjoin_testnet.md) - -**Your Task:** -1. Set up a regtest environment with 2 wallets -2. Install and configure the Payjoin Dev Kit -3. Wallet A (sender): construct an original PSBT paying Wallet B -4. Wallet B (receiver): add an input, modify outputs, sign, return -5. Wallet A: verify the modified PSBT, sign, broadcast -6. Inspect the final transaction: can you tell which inputs belong to which wallet? -7. Compare with a non-Payjoin transaction between the same wallets - -### Discussion Questions - -1. What coin selection strategy should the receiver use when adding inputs? -2. How does Payjoin help receivers consolidate UTXOs "for free"? -3. What checks must the sender perform on the receiver's modified PSBT to prevent attacks? - ---- - -## Session 3: Contributing to Payjoin - -### Overview - -You've built with Payjoin. Now contribute to it. This session walks through the payjoin-rust codebase, BTCPay Server's implementation, and helps you find your first contribution opportunity. - -### Topics - -**payjoin-rust Codebase Walkthrough** -- Repository structure: `payjoin/`, `payjoin-cli/`, `payjoin-directory/` -- Key modules: `send/`, `receive/`, `ohttp/`, `directory/` -- The OHTTP (Oblivious HTTP) relay for V2 privacy -- Test suite: integration tests, test vectors -- Open issues and contribution areas - -**BTCPay Server's Payjoin Implementation** -- How BTCPay integrates Payjoin for merchant payments -- The `PayjoinClient` and `PayjoinEndpointController` classes -- Configuration options and user experience -- Open issues related to Payjoin in BTCPay - -**Bull Bitcoin's BIP77 Integration** -- First commercially available wallet with Payjoin V2 -- Mobile wallet architecture considerations -- UX decisions: when to offer Payjoin, how to handle failures -- Lessons from production deployment - -### Exercise - -**Your First Payjoin PR:** -1. Pick one repository: - - [payjoin/rust-payjoin](https://github.com/payjoin/rust-payjoin) - Rust - - [btcpayserver/btcpayserver](https://github.com/btcpayserver/btcpayserver) - C# - - Any wallet integrating BIP77/78 -2. Browse open issues - look for "good first issue", documentation improvements, or test coverage gaps -3. Fork, implement, write tests -4. Submit your PR - -### Assessment - -**Deliverable**: A submitted PR to a Payjoin-related project. Bring the PR link to the next session. - ---- - -*Code Orange Dev School | [codeorange.dev](https://codeorange.dev) | CC0 1.0 Universal* diff --git a/privacy-track/module-03-payjoin/exercises/payjoin_testnet.md b/privacy-track/module-03-payjoin/exercises/payjoin_testnet.md deleted file mode 100644 index 23a6362..0000000 --- a/privacy-track/module-03-payjoin/exercises/payjoin_testnet.md +++ /dev/null @@ -1,184 +0,0 @@ -# Payjoin Testnet Exercise - -> Build and send a Payjoin transaction on regtest. - -## Setup - -### Prerequisites - -- Bitcoin Core installed and running in regtest mode -- Rust toolchain installed (`rustup`) -- Payjoin Dev Kit cloned: `git clone https://github.com/payjoin/rust-payjoin` - -### Start Your Regtest Environment - -```bash -# Start bitcoind in regtest -bitcoind -regtest -daemon - -# Create two wallets (sender and receiver) -bitcoin-cli -regtest createwallet "sender" -bitcoin-cli -regtest createwallet "receiver" - -# Mine blocks to the sender (need 101 for maturity) -SENDER_ADDR=$(bitcoin-cli -regtest -rpcwallet=sender getnewaddress) -bitcoin-cli -regtest generatetoaddress 101 $SENDER_ADDR - -# Check sender balance -bitcoin-cli -regtest -rpcwallet=sender getbalance -# Should show 50.00000000 - -# Fund the receiver too (they need a UTXO to contribute to the Payjoin) -RECEIVER_ADDR=$(bitcoin-cli -regtest -rpcwallet=receiver getnewaddress) -bitcoin-cli -regtest -rpcwallet=sender sendtoaddress $RECEIVER_ADDR 5.0 -bitcoin-cli -regtest generatetoaddress 1 $SENDER_ADDR -``` - ---- - -## Exercise 1: Manual Payjoin (Step by Step) - -### Step 1: Receiver Creates a Payment Request - -```bash -# Receiver generates a fresh address for the payment -PAYMENT_ADDR=$(bitcoin-cli -regtest -rpcwallet=receiver getnewaddress) -echo "Payment address: $PAYMENT_ADDR" -``` - -### Step 2: Sender Creates the Original PSBT - -```bash -# Sender creates a PSBT paying 1 BTC to the receiver -ORIG_PSBT=$(bitcoin-cli -regtest -rpcwallet=sender walletcreatefundedpsbt \ - '[]' "[{\"$PAYMENT_ADDR\": 1.0}]" 0 '{"includeWatching": true}' | jq -r '.psbt') - -echo "Original PSBT: $ORIG_PSBT" - -# Decode it to see the structure -bitcoin-cli -regtest decodepsbt $ORIG_PSBT -``` - -**YOUR TASK**: Examine the decoded PSBT. Answer: -- How many inputs does it have? -- How many outputs? -- Which output is the payment and which is change? -- If a chain analyst sees this, what can they deduce? - -### Step 3: Sender Signs the Original PSBT - -```bash -SIGNED_ORIG=$(bitcoin-cli -regtest -rpcwallet=sender walletprocesspsbt $ORIG_PSBT | jq -r '.psbt') -echo "Signed original PSBT: $SIGNED_ORIG" -``` - -### Step 4: Receiver Adds Their Input (The Payjoin Magic) - -This is where Payjoin happens. The receiver contributes one of their own UTXOs. - -```bash -# Receiver lists their UTXOs -bitcoin-cli -regtest -rpcwallet=receiver listunspent - -# Pick one of the receiver's UTXOs (note the txid, vout, and amount) -# YOUR CODE: Fill in the UTXO details from the listunspent output -RECEIVER_UTXO_TXID="" -RECEIVER_UTXO_VOUT=0 # fill in -RECEIVER_UTXO_AMOUNT=5.0 # fill in -``` - -**YOUR TASK**: Now manually construct the modified Payjoin PSBT. - -The receiver needs to: -1. Add their UTXO as an additional input -2. Increase their payment output by the value of their added input -3. The change output from the sender stays the same - -```bash -# Create the Payjoin PSBT with both sender's and receiver's inputs -# The receiver's output increases from 1.0 BTC to 1.0 + RECEIVER_UTXO_AMOUNT -# (minus the receiver's share of the fee) - -# YOUR CODE HERE: -# Use walletcreatefundedpsbt or createpsbt to build the modified transaction -# Hint: You'll need to include: -# - All inputs from the original PSBT -# - The receiver's additional UTXO as a new input -# - The payment output (now 1.0 + receiver's UTXO amount) -# - The sender's change output (unchanged) -``` - -### Step 5: Both Sign the Payjoin PSBT - -```bash -# Receiver signs their input -RECEIVER_SIGNED=$(bitcoin-cli -regtest -rpcwallet=receiver walletprocesspsbt $PAYJOIN_PSBT | jq -r '.psbt') - -# Sender signs their input(s) -BOTH_SIGNED=$(bitcoin-cli -regtest -rpcwallet=sender walletprocesspsbt $RECEIVER_SIGNED | jq -r '.psbt') - -# Finalize -FINAL_TX=$(bitcoin-cli -regtest finalizepsbt $BOTH_SIGNED | jq -r '.hex') -``` - -### Step 6: Broadcast and Analyze - -```bash -# Broadcast -TXID=$(bitcoin-cli -regtest sendrawtransaction $FINAL_TX) -echo "Payjoin txid: $TXID" - -# Mine a block -bitcoin-cli -regtest generatetoaddress 1 $SENDER_ADDR - -# Decode the final transaction -bitcoin-cli -regtest getrawtransaction $TXID true -``` - -**YOUR TASK**: Analyze the final transaction: -1. How many inputs does it have? -2. Can you tell which input belongs to the sender and which to the receiver? -3. Apply the common-input-ownership heuristic - what would a chain analyst conclude? -4. Is that conclusion correct? - ---- - -## Exercise 2: Compare Normal vs Payjoin - -Create two transactions side by side: - -1. A **normal transaction**: sender pays receiver 1 BTC (no Payjoin) -2. A **Payjoin transaction**: sender pays receiver 1 BTC (with receiver input) - -For each transaction, document: - -| Property | Normal TX | Payjoin TX | -|----------|-----------|------------| -| Number of inputs | | | -| Number of outputs | | | -| Can identify change output? | | | -| CIOH conclusion | | | -| CIOH accuracy | | | -| Identifiable as Payjoin? | | | - ---- - -## Exercise 3: Discussion Questions - -Write 2-3 paragraphs on each: - -1. **What coin selection strategy should the receiver use when adding their input?** Should they add their largest UTXO? Smallest? Random? What are the privacy trade-offs? - -2. **What checks must the sender perform on the receiver's modified PSBT?** List every verification step needed to prevent the receiver from stealing funds or degrading the sender's privacy. - -3. **If you were integrating Payjoin into a mobile wallet, what UX decisions would you make?** Should it be automatic? Opt-in? How do you handle failures? - ---- - -## Submission - -Post your transaction analysis and discussion answers in the Discord `#privacy-track` channel before the next session. - ---- - -*Code Orange Dev School | [codeorange.dev](https://codeorange.dev) | CC0 1.0 Universal* diff --git a/privacy-track/module-03-payjoin/exercises/pdk_integration/Cargo.toml b/privacy-track/module-03-payjoin/exercises/pdk_integration/Cargo.toml deleted file mode 100644 index 7c082ff..0000000 --- a/privacy-track/module-03-payjoin/exercises/pdk_integration/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pdk-exercise" -version = "0.1.0" -edition = "2021" -description = "Code Orange Privacy Track — Payjoin Dev Kit integration exercise" -license = "CC0-1.0" - -[dependencies] -payjoin = { version = "0.21", features = ["send", "receive", "v2"] } -bitcoin = "0.32" -bitcoincore-rpc = "0.19" -tokio = { version = "1", features = ["full"] } -anyhow = "1" diff --git a/privacy-track/module-03-payjoin/exercises/pdk_integration/src/main.rs b/privacy-track/module-03-payjoin/exercises/pdk_integration/src/main.rs deleted file mode 100644 index d051dc4..0000000 --- a/privacy-track/module-03-payjoin/exercises/pdk_integration/src/main.rs +++ /dev/null @@ -1,322 +0,0 @@ -// Bitcoin Privacy Track — Module 3: Payjoin Dev Kit Integration -// Code Orange Dev School | codeorange.dev -// -// Build a working Payjoin sender and receiver using the Payjoin Dev Kit (PDK). -// -// This exercise connects to a local Bitcoin Core regtest node and performs -// a real Payjoin transaction, demonstrating the full BIP77/78 flow. -// -// Prerequisites: -// - Bitcoin Core running in regtest mode with two wallets ("sender", "receiver") -// - Rust toolchain (1.70+) -// - Completed the manual Payjoin exercise (payjoin_testnet.md) -// -// Setup: -// bitcoind -regtest -daemon -server -rpcuser=bitcoin -rpcpassword=bitcoin -// bitcoin-cli -regtest createwallet "sender" -// bitcoin-cli -regtest createwallet "receiver" -// bitcoin-cli -regtest -rpcwallet=sender generatetoaddress 101 $(bitcoin-cli -regtest -rpcwallet=sender getnewaddress) -// -// Run: -// cargo run -// -// This exercise has 5 parts. Complete each TODO section. - -use anyhow::{Context, Result}; -use bitcoin::Amount; -use bitcoincore_rpc::{Auth, Client, RpcApi}; - -/// Connect to a Bitcoin Core regtest wallet. -fn connect_wallet(wallet_name: &str) -> Result { - let url = format!("http://127.0.0.1:18443/wallet/{}", wallet_name); - let auth = Auth::UserPass("bitcoin".to_string(), "bitcoin".to_string()); - Client::new(&url, auth).context(format!("Failed to connect to wallet '{}'", wallet_name)) -} - -// ============================================================ -// Exercise 1: Verify Environment -// ============================================================ -// -// Confirm both wallets exist, have funds, and are ready for Payjoin. - -fn verify_environment() -> Result<()> { - println!("=== Exercise 1: Verify Environment ===\n"); - - let sender = connect_wallet("sender")?; - let receiver = connect_wallet("receiver")?; - - // TODO: Get and print the balance of both wallets - // Use: sender.get_balance(None, None)? - // - // If the sender has no funds, mine 101 blocks: - // let addr = sender.get_new_address(None, None)?; - // sender.generate_to_address(101, &addr.assume_checked())?; - // - // Fund the receiver with 5 BTC from the sender: - // let recv_addr = receiver.get_new_address(None, None)?; - // sender.send_to_address(&recv_addr.assume_checked(), Amount::from_btc(5.0)?, ...)?; - // // Mine a block to confirm - // let addr = sender.get_new_address(None, None)?; - // sender.generate_to_address(1, &addr.assume_checked())?; - - // YOUR CODE HERE - - println!(" Environment verified!\n"); - Ok(()) -} - -// ============================================================ -// Exercise 2: Create the Original PSBT (Sender Side) -// ============================================================ -// -// The sender creates a standard transaction (PSBT) that pays the receiver. -// This is the "original" transaction before Payjoin modification. - -fn create_original_psbt() -> Result { - println!("=== Exercise 2: Create Original PSBT ===\n"); - - let sender = connect_wallet("sender")?; - let receiver = connect_wallet("receiver")?; - - // TODO: - // 1. Get a fresh address from the receiver - // let payment_addr = receiver.get_new_address(None, None)?; - // - // 2. Create a funded PSBT from the sender paying 1 BTC to the receiver - // Use: sender.wallet_create_funded_psbt( - // &[], // inputs (empty = auto-select) - // &[outputs], // outputs - // None, // locktime - // Some(options), // options - // None, // bip32derivs - // )?; - // - // 3. Print the PSBT details: - // - Number of inputs - // - Number of outputs - // - Fee amount - // - Which output is the payment and which is change - // - // 4. Sign the PSBT with the sender's wallet - // let signed = sender.wallet_process_psbt(&psbt, ...)?; - // - // 5. Return the signed PSBT as a base64 string - - // YOUR CODE HERE - - todo!("Implement create_original_psbt") -} - -// ============================================================ -// Exercise 3: Receiver Modifies the PSBT (The Payjoin) -// ============================================================ -// -// The receiver takes the sender's PSBT and adds their own input. -// This is the core Payjoin operation. - -fn receiver_modify_psbt(original_psbt: &str) -> Result { - println!("=== Exercise 3: Receiver Modifies PSBT ===\n"); - - let receiver = connect_wallet("receiver")?; - - // TODO: - // 1. Decode the original PSBT to inspect it - // let decoded = receiver.decode_psbt(original_psbt)?; - // Print: "Original TX has N inputs and M outputs" - // - // 2. List the receiver's UTXOs - // let utxos = receiver.list_unspent(None, None, None, None, None)?; - // Pick one UTXO to contribute to the Payjoin - // Print: "Receiver contributing UTXO: {txid}:{vout} ({amount} BTC)" - // - // 3. This is the hard part — construct a new PSBT that includes: - // a) All inputs from the original PSBT - // b) The receiver's additional input - // c) The payment output (increased by the receiver's input value) - // d) The sender's change output (unchanged) - // - // In production, the Payjoin Dev Kit handles this automatically. - // For this exercise, you can either: - // - Use PDK's receive module (preferred) - // - Manually construct the modified PSBT using createpsbt RPC - // - // 4. Sign the receiver's input - // let receiver_signed = receiver.wallet_process_psbt(&modified_psbt, ...)?; - // - // 5. Print the modified PSBT details: - // - New number of inputs (should be original + 1) - // - Updated output amounts - // - "Chain analyst sees: N inputs, M outputs — applies CIOH — WRONG" - // - // 6. Return the receiver-signed modified PSBT - - // YOUR CODE HERE - - todo!("Implement receiver_modify_psbt") -} - -// ============================================================ -// Exercise 4: Sender Verifies and Signs (Security Checks) -// ============================================================ -// -// The sender MUST verify the receiver's modifications before signing. -// A malicious receiver could steal funds if the sender doesn't check. - -fn sender_verify_and_sign(original_psbt: &str, modified_psbt: &str) -> Result { - println!("=== Exercise 4: Sender Verifies and Signs ===\n"); - - let sender = connect_wallet("sender")?; - - // TODO: Implement ALL of these verification checks. - // These are critical for security — a real wallet must perform every one. - // - // VERIFICATION CHECKLIST: - // - // 1. SENDER'S INPUTS UNCHANGED: - // All inputs from the original PSBT must still be present - // in the modified PSBT with identical scriptPubKeys. - // "Check: sender's inputs are preserved" - // - // 2. SENDER'S PAYMENT OUTPUT UNCHANGED: - // The output paying the receiver must have the same or greater value. - // (Greater is fine — the receiver is adding value.) - // "Check: payment output value >= original" - // - // 3. SENDER'S CHANGE OUTPUT UNCHANGED: - // The sender's change output must have the same value. - // If the receiver reduced the sender's change, they're stealing. - // "Check: sender's change output preserved" - // - // 4. NO NEW OUTPUTS TO UNKNOWN ADDRESSES: - // The modified PSBT should not add outputs to addresses - // the sender doesn't recognize. (The receiver's output increasing - // is expected; a new third output is suspicious.) - // "Check: no unexpected new outputs" - // - // 5. FEE IS REASONABLE: - // The total fee should not increase unreasonably. - // The receiver may slightly increase the fee (they added an input), - // but a massive fee increase could be an attack. - // "Check: fee increase is reasonable" - // - // 6. TRANSACTION IS VALID: - // The modified PSBT must be a valid Bitcoin transaction. - // - // If all checks pass: - // Sign the sender's inputs: sender.wallet_process_psbt(&modified_psbt, ...)? - // Finalize: sender.finalize_psbt(&both_signed, None)? - // Return the final raw transaction hex - // - // If any check fails: - // Print which check failed and return an error - // The sender should broadcast the ORIGINAL transaction instead - - // YOUR CODE HERE - - todo!("Implement sender_verify_and_sign") -} - -// ============================================================ -// Exercise 5: Broadcast and Analyze -// ============================================================ -// -// Broadcast the Payjoin transaction, then analyze it from a -// chain analyst's perspective. - -fn broadcast_and_analyze(final_tx_hex: &str) -> Result<()> { - println!("=== Exercise 5: Broadcast and Analyze ===\n"); - - let sender = connect_wallet("sender")?; - - // TODO: - // 1. Broadcast the transaction - // let txid = sender.send_raw_transaction(final_tx_hex)?; - // println!(" Payjoin txid: {}", txid); - // - // 2. Mine a block - // let addr = sender.get_new_address(None, None)?; - // sender.generate_to_address(1, &addr.assume_checked())?; - // - // 3. Decode the confirmed transaction - // let tx = sender.get_raw_transaction_info(&txid, None)?; - // - // 4. Print the chain analyst's view: - // println!("\n === Chain Analyst's View ==="); - // println!(" Inputs: {}", tx.vin.len()); - // println!(" Outputs: {}", tx.vout.len()); - // println!(); - // println!(" Applying CIOH: 'All {} inputs belong to the same entity'", tx.vin.len()); - // println!(" This conclusion is: WRONG"); - // println!(); - // println!(" Applying change detection:"); - // // Analyze which output looks like change - // // The analyst will likely identify the wrong entity as the sender - // println!(); - // println!(" The Payjoin has created ambiguity:"); - // println!(" - Possible interpretation 1: ..."); - // println!(" - Possible interpretation 2: ..."); - // println!(" - Possible interpretation 3: ..."); - // println!(" The analyst cannot determine which is correct."); - // - // 5. Now create a NORMAL (non-Payjoin) transaction for comparison - // Send the same amount to the same receiver, but without Payjoin - // Broadcast and decode it - // Print the same analysis - // Show how the normal transaction reveals much more information - - // YOUR CODE HERE - - todo!("Implement broadcast_and_analyze") -} - -// ============================================================ -// Main -// ============================================================ - -fn main() -> Result<()> { - println!("================================================================"); - println!("Bitcoin Privacy Track — Payjoin Dev Kit Integration"); - println!("Code Orange Dev School | codeorange.dev"); - println!("================================================================\n"); - - // Exercise 1: Verify environment - verify_environment()?; - - // Exercise 2: Sender creates original PSBT - let original_psbt = create_original_psbt()?; - - // Exercise 3: Receiver modifies the PSBT - let modified_psbt = receiver_modify_psbt(&original_psbt)?; - - // Exercise 4: Sender verifies and signs - let final_tx = sender_verify_and_sign(&original_psbt, &modified_psbt)?; - - // Exercise 5: Broadcast and analyze - broadcast_and_analyze(&final_tx)?; - - println!("\n================================================================"); - println!("REFLECTION QUESTIONS:"); - println!("================================================================"); - println!(); - println!("1. In Exercise 4, you implemented 5 security checks."); - println!(" Which check is the most important? What happens if"); - println!(" a wallet skips it?"); - println!(); - println!("2. The Payjoin Dev Kit handles the PSBT modification"); - println!(" automatically. What advantage does manual implementation"); - println!(" (as in this exercise) give you as a developer?"); - println!(); - println!("3. How would you modify this code to use BIP77 (V2)"); - println!(" with a Payjoin Directory instead of direct communication?"); - println!(); - println!("4. If you were integrating Payjoin into a mobile wallet,"); - println!(" what UX would you show the user during the PSBT"); - println!(" exchange? Should the user even know a Payjoin is"); - println!(" happening?"); - println!(); - println!("================================================================"); - println!("Done! Bring your code and analysis to the weekly session."); - println!("================================================================"); - - Ok(()) -} diff --git a/privacy-track/module-04-wallet-privacy/README.md b/privacy-track/module-04-wallet-privacy/README.md deleted file mode 100644 index 6418803..0000000 --- a/privacy-track/module-04-wallet-privacy/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# Module 4: Privacy-Preserving Wallet Development - -> 3 sessions - Coin selection, compact block filters, CoinJoin, and the future of on-chain privacy. - ---- - -## Session 1: Transaction Construction for Privacy - -### Overview - -Even without Silent Payments or Payjoin, the way a wallet constructs a transaction reveals enormous amounts of information. This session covers the subtle fingerprints that wallets leave in every transaction and how privacy-conscious wallet developers can eliminate them. - -### Topics - -**Coin Selection Algorithms** -- **Largest-first**: simple but creates obvious change outputs - bad for privacy -- **Branch and bound**: tries to find exact-match combinations (no change) - great for privacy -- **Random selection**: adds entropy but wastes fees -- **Privacy-optimized**: avoid combining UTXOs from different sources, prefer spending whole UTXOs -- The trade-off: fee efficiency vs privacy - you can't always optimize both -- BDK's coin selection module and how to extend it - -**Change Output Handling** -- **Avoiding change entirely**: branch-and-bound to find exact-match input sets -- **Change output script type**: must match the payment output's script type (P2WPKH → P2WPKH) -- **Change output position**: randomize (don't always put change first or last) -- **Dust avoidance**: tiny change outputs are uneconomical and reveal information -- **Change avoidance strategies**: slightly overpay fees to eliminate change - -**Fee Fingerprinting and Timing Analysis** -- Different wallets use different fee estimation methods -- Bitcoin Core uses `estimatesmartfee` - a recognizable fee curve -- Some wallets always use round fee rates (5 sat/vB, 10 sat/vB) -- Transaction broadcast timing can reveal timezone/geography -- Mempool snooping: observing which node first relays a transaction - -**Wallet Fingerprints: nLockTime, nSequence, Version** -- `nLockTime`: Bitcoin Core sets this to current block height (anti-fee-sniping) - identifiable -- `nSequence`: different values reveal which wallet created the transaction -- `version`: most wallets use version 2, but some still use version 1 -- Input ordering: BIP69 (deprecated) vs random vs deterministic -- Output ordering: random is best for privacy -- These fingerprints are invisible to users but obvious to chain analysts - -### Exercise - -**Wallet Fingerprint Analysis:** -1. Take 5 transactions from mempool.space (mainnet) -2. For each transaction, identify: - - Likely wallet software (based on fingerprints above) - - Change output (using heuristics from Module 1) - - Fee estimation method -3. Compare a Bitcoin Core transaction vs an Electrum transaction vs a mobile wallet transaction -4. Document which fingerprints are most reliable for identification - -### Discussion Questions - -1. If you were building a new wallet from scratch, what transaction construction defaults would you choose for maximum privacy? -2. Should wallets sacrifice fee efficiency for privacy? How should this trade-off be presented to users? -3. Why is output ordering important? What information does a deterministic order leak? - -### Reading - -- [Bitcoin Wiki: Privacy - Wallet Fingerprinting](https://en.bitcoin.it/wiki/Privacy#Wallet_fingerprinting) -- [Bitcoin Optech: Coin Selection](https://bitcoinops.org/en/topics/coin-selection/) -- [Murch: Coin Selection with Leverage](https://murch.one/erhardt2016coinselection.pdf) - ---- - -## Session 2: Compact Block Filters & Light Client Privacy - -### Overview - -Full nodes offer the best privacy but aren't practical for everyone. Light clients need to query the network for their transactions - but how do they do this without revealing which addresses they own? Compact block filters (BIP157/158) offer the best current solution. This session covers how they work and the projects building them. - -### Topics - -**BIP157/158 - How Compact Block Filters Work** -- The problem: SPV clients (BIP37) tell full nodes exactly which addresses they're watching - terrible for privacy -- The solution: full nodes create a small "filter" for each block containing all the scripts in that block -- The filter is a probabilistic data structure - it can tell you "this block MIGHT contain your transaction" -- Client downloads filters for every block (small: ~20KB per filter) and checks locally -- Only downloads the full block when the filter matches - no privacy leak to the serving node - -**Golomb-Rice Coded Sets (GCS)** -- The compression algorithm used in BIP158 filters -- How it encodes a set of items with configurable false-positive rate -- The parameters chosen for BIP158: P=19 (false positive rate ~1/524,288), M=784,931 -- Why GCS is more efficient than Bloom filters for this use case -- The bandwidth trade-off: filter size vs false positive rate - -**Kyoto Light Client Project** -- [rustaceanrob/kyoto](https://github.com/rustaceanrob/kyoto) - Rust implementation -- Implements BIP157/158 compact block filter protocol -- Critical for Silent Payments: light clients need this to scan efficiently -- Integration with BDK for wallet development -- Current status and contribution opportunities - -**Privacy Comparison: SPV vs Compact Block Filters vs Full Node** - -| Property | SPV (BIP37) | Compact Filters (BIP157) | Full Node | -|----------|-------------|-------------------------|-----------| -| Privacy | Very poor - server knows your addresses | Good - server can't identify your addresses | Perfect - you validate everything locally | -| Bandwidth | Low | Medium (~20KB/block filter) | High (full blocks) | -| Computation | Minimal | Low (filter matching) | High (full validation) | -| Trust | Trusts server to provide correct data | Trusts server for filter, verifies blocks | Trustless | -| Suitable for mobile | Yes | Yes | No (usually) | - -### Exercise - -**Compact Block Filter Exploration:** -1. Using Bitcoin Core (`getblockfilter` RPC), retrieve the compact block filter for a recent block -2. Decode the filter and understand its structure -3. Check if a known address appears in the filter -4. Calculate the false positive rate: check 1000 random addresses against the filter -5. Compare the filter size to the full block size - what's the compression ratio? - -### Discussion Questions - -1. Why did BIP37 (Bloom filters) fail for privacy? What specific information does it leak? -2. If a compact block filter has a false positive rate of 1/524,288, how many false positives per day on mainnet? -3. Could a malicious node send you incorrect filters? How do you defend against this? -4. Why are compact block filters critical for Silent Payments adoption on mobile? - -### Reading - -- [BIP157: Client Side Block Filtering](https://bips.dev/157/) -- [BIP158: Compact Block Filters for Light Clients](https://bips.dev/158/) -- [Bitcoin Optech: Compact Block Filters](https://bitcoinops.org/en/topics/compact-block-filters/) -- [Kyoto Light Client](https://github.com/rustaceanrob/kyoto) - ---- - -## Session 3: CoinJoin, CoinSwap, and Beyond - -### Overview - -The final session covers the most powerful privacy tools available today and the frontier of what's coming next. CoinJoin mixes your UTXOs with other users. CoinSwap swaps them atomically. And future protocol improvements like cross-input signature aggregation (CISA) could make privacy nearly free. - -### Topics - -**CoinJoin: Equal-Output Mixing** -- The original idea: multiple users contribute inputs and receive equal-value outputs -- No coordinator can steal funds (they only coordinate, never hold keys) -- WabiSabi protocol (used by Wasabi Wallet, Ginger Wallet): - - Eliminates the fixed output size limitation - - Keyed-verification anonymous credentials for input registration - - Variable output amounts with privacy guarantees -- The anonymity set: how many equal outputs exist in the mix -- Limitations: equal outputs are recognizable as CoinJoin; requires other participants - -**CoinSwap: Atomic Swaps for Privacy** -- Unlike CoinJoin, CoinSwap transactions look like normal payments on-chain -- Alice pays Bob, Bob pays Alice - two separate transactions, atomically linked via hash locks -- Chris Belcher's "Teleport Transactions" implementation -- Multi-hop CoinSwap: route through multiple intermediaries for stronger privacy -- The advantage over CoinJoin: no recognizable on-chain pattern - -**JoinMarket NG and Its Approach** -- JoinMarket: decentralized CoinJoin marketplace with makers (liquidity providers) and takers -- JoinMarket NG: next generation, funded by OpenSats -- Fidelity bonds: makers lock bitcoin to prove they're not Sybil attackers -- How it differs from centralized coordinators (Wasabi, Ginger) - -**The Future: Protocol-Level Privacy** -- **Cross-Input Signature Aggregation (CISA)**: aggregate all signatures in a transaction into one - - Makes CoinJoin cheaper (fewer bytes = lower fees) - - Economic incentive for privacy - mixing becomes the cheapest option - - Requires a soft fork -- **Taproot-enabled privacy**: all spends (single-sig, multisig, HTLC, etc.) look identical -- **Schnorr signature aggregation**: enables MuSig2, threshold signatures -- **Ark**: off-chain payment protocol with on-chain settlement privacy -- The endgame: privacy by default, not privacy by choice - -### Exercise - -**Privacy Tool Comparison:** -1. Send 3 transactions on signet/regtest: - - A normal P2WPKH transaction - - A transaction with deliberately good privacy practices (no change, random output order, current-height nLockTime) - - A CoinJoin-style transaction (simulate with multiple wallets) -2. Analyze all 3 using mempool.space or your own block explorer -3. For each transaction, rate: How much can a chain analyst learn? What is ambiguous? -4. Write a 1-page comparison of the three approaches - -### Discussion Questions - -1. CoinJoin creates equal outputs that are recognizable. Does this help or hurt privacy? -2. CoinSwap is invisible on-chain but requires a counterparty. What's the adoption bottleneck? -3. If CISA is activated, making CoinJoin cheaper than normal transactions, would privacy become the default? Why or why not? -4. What privacy improvements can be made today without any protocol changes? - -### Assessment - -**Final Project**: Write a 1,000-word report: **"A Privacy Roadmap for Bitcoin: What Exists, What's Missing, and Where I Want to Contribute."** Include specific projects and issues you plan to work on. This serves as your contribution plan for the post-track Contribution Sprint. - -### Reading - -- [Bitcoin Optech: CoinJoin](https://bitcoinops.org/en/topics/coinjoin/) -- [WabiSabi Paper](https://github.com/zkSNACKs/WabiSabi/blob/master/WabiSabi.pdf) -- [Chris Belcher: CoinSwap Design](https://gist.github.com/chris-belcher/9144bd57a91c194e332fb5ca371d0964) -- [Bitcoin Optech: CISA](https://bitcoinops.org/en/topics/cross-input-signature-aggregation/) - ---- - -*Code Orange Dev School | [codeorange.dev](https://codeorange.dev) | CC0 1.0 Universal* diff --git a/privacy-track/module-04-wallet-privacy/exercises/.gitkeep b/privacy-track/module-04-wallet-privacy/exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/privacy-track/module-04-wallet-privacy/exercises/coin_selection_simulator.py b/privacy-track/module-04-wallet-privacy/exercises/coin_selection_simulator.py deleted file mode 100644 index e084cb5..0000000 --- a/privacy-track/module-04-wallet-privacy/exercises/coin_selection_simulator.py +++ /dev/null @@ -1,540 +0,0 @@ -""" -Bitcoin Privacy Track — Module 4, Session 1: Coin Selection Simulator -Code Orange Dev School | codeorange.dev - -Build and compare coin selection algorithms, measuring their privacy -and fee-efficiency trade-offs. - -You will implement 4 coin selection strategies, run them against -realistic UTXO sets, and score each one on privacy and cost. - -Prerequisites: -- Understanding of UTXOs and transaction construction -- Python 3.8+ - -Instructions: -- Complete each function by filling in the code where indicated -- Run: python3 coin_selection_simulator.py -- Bring your results and analysis to the weekly session -""" - -import random -import hashlib -from typing import List, Tuple, Optional, Dict -from dataclasses import dataclass, field -from enum import Enum - - -# ============================================================ -# Data Structures -# ============================================================ - -class ScriptType(Enum): - P2PKH = "p2pkh" - P2SH_P2WPKH = "p2sh-p2wpkh" - P2WPKH = "p2wpkh" - P2TR = "p2tr" - - -@dataclass -class UTXO: - """Represents an unspent transaction output.""" - txid: str - vout: int - value: int # satoshis - script_type: ScriptType - label: str = "" # e.g., "exchange", "salary", "coinjoin", "unknown" - confirmations: int = 6 - address: str = "" - - @property - def input_weight(self) -> int: - """Estimated weight units for spending this UTXO.""" - weights = { - ScriptType.P2PKH: 592, # ~148 vbytes - ScriptType.P2SH_P2WPKH: 364, # ~91 vbytes - ScriptType.P2WPKH: 272, # ~68 vbytes - ScriptType.P2TR: 230, # ~57.5 vbytes - } - return weights.get(self.script_type, 272) - - def __repr__(self): - btc = self.value / 100_000_000 - return f"UTXO({btc:.8f} BTC, {self.script_type.value}, label={self.label})" - - -@dataclass -class CoinSelectionResult: - """Result of a coin selection algorithm.""" - selected: List[UTXO] - change_amount: int # 0 if no change needed - fee: int - total_input: int - target_amount: int - - @property - def num_inputs(self) -> int: - return len(self.selected) - - @property - def has_change(self) -> bool: - return self.change_amount > 0 - - @property - def waste(self) -> int: - """Waste metric: excess fees + cost of creating/spending change.""" - CHANGE_OUTPUT_COST = 31 * 4 # ~31 vbytes for P2WPKH change output - CHANGE_SPEND_COST = 68 * 4 # ~68 vbytes to spend the change later - change_cost = (CHANGE_OUTPUT_COST + CHANGE_SPEND_COST) if self.has_change else 0 - excess = self.total_input - self.target_amount - self.fee - return change_cost + max(0, excess - self.change_amount) - - -# ============================================================ -# Helper: Fee Calculation -# ============================================================ - -def estimate_tx_fee(num_inputs: int, num_outputs: int, - input_types: List[ScriptType], - fee_rate: float = 5.0) -> int: - """Estimate transaction fee in satoshis. - - Args: - num_inputs: number of inputs - num_outputs: number of outputs - input_types: script types of inputs (for weight calculation) - fee_rate: fee rate in sat/vB - - Returns: - estimated fee in satoshis - """ - # Transaction overhead: 10.5 vbytes (version, marker, flag, locktime) - overhead_weight = 42 - - # Input weights - input_weight = sum( - {ScriptType.P2PKH: 592, ScriptType.P2SH_P2WPKH: 364, - ScriptType.P2WPKH: 272, ScriptType.P2TR: 230}.get(t, 272) - for t in input_types - ) - - # Output weights (assume P2WPKH outputs: 31 vbytes each) - output_weight = num_outputs * 124 # 31 * 4 - - total_weight = overhead_weight + input_weight + output_weight - total_vbytes = (total_weight + 3) // 4 # round up - - return int(total_vbytes * fee_rate) - - -# ============================================================ -# Generate Realistic UTXO Sets -# ============================================================ - -def generate_utxo_set(scenario: str = "typical") -> List[UTXO]: - """Generate a realistic UTXO set for testing. - - Scenarios: - 'typical': mixed UTXOs from various sources (20 UTXOs) - 'exchange_heavy': mostly large UTXOs from exchange withdrawals - 'dust_heavy': many small UTXOs from mining/faucets - 'privacy_conscious': UTXOs from CoinJoin with clean labels - """ - random.seed(42) # Reproducible - utxos = [] - - def make_utxo(value, script_type, label, confs=None): - txid = hashlib.sha256(f"{value}{label}{len(utxos)}".encode()).hexdigest() - return UTXO( - txid=txid, vout=random.randint(0, 3), - value=value, script_type=script_type, label=label, - confirmations=confs or random.randint(1, 1000), - address=f"bc1q_addr_{len(utxos):04d}" - ) - - if scenario == "typical": - # Mixed wallet: salary deposits, exchange withdrawals, received payments - utxos = [ - make_utxo(5_000_000, ScriptType.P2WPKH, "salary"), # 0.05 BTC - make_utxo(2_500_000, ScriptType.P2WPKH, "salary"), # 0.025 BTC - make_utxo(10_000_000, ScriptType.P2WPKH, "exchange"), # 0.1 BTC - make_utxo(500_000, ScriptType.P2WPKH, "friend_payment"), # 0.005 BTC - make_utxo(1_000_000, ScriptType.P2TR, "donation"), # 0.01 BTC - make_utxo(50_000, ScriptType.P2WPKH, "dust"), # 0.0005 BTC - make_utxo(3_000_000, ScriptType.P2WPKH, "salary"), # 0.03 BTC - make_utxo(750_000, ScriptType.P2SH_P2WPKH, "old_wallet"), # 0.0075 BTC - make_utxo(20_000_000, ScriptType.P2WPKH, "exchange"), # 0.2 BTC - make_utxo(100_000, ScriptType.P2WPKH, "lightning_close"), # 0.001 BTC - make_utxo(8_000_000, ScriptType.P2TR, "coinjoin"), # 0.08 BTC - make_utxo(200_000, ScriptType.P2WPKH, "unknown"), # 0.002 BTC - make_utxo(15_000_000, ScriptType.P2WPKH, "salary"), # 0.15 BTC - make_utxo(400_000, ScriptType.P2WPKH, "friend_payment"), # 0.004 BTC - make_utxo(6_000_000, ScriptType.P2TR, "exchange"), # 0.06 BTC - make_utxo(30_000, ScriptType.P2WPKH, "dust"), # 0.0003 BTC - make_utxo(1_500_000, ScriptType.P2WPKH, "unknown"), # 0.015 BTC - make_utxo(4_000_000, ScriptType.P2WPKH, "salary"), # 0.04 BTC - make_utxo(900_000, ScriptType.P2TR, "coinjoin"), # 0.009 BTC - make_utxo(7_000_000, ScriptType.P2WPKH, "exchange"), # 0.07 BTC - ] - - elif scenario == "exchange_heavy": - for i in range(8): - utxos.append(make_utxo( - random.choice([10_000_000, 20_000_000, 50_000_000, 100_000_000]), - ScriptType.P2WPKH, "exchange" - )) - - elif scenario == "dust_heavy": - for i in range(50): - utxos.append(make_utxo( - random.randint(5_000, 100_000), - ScriptType.P2WPKH, random.choice(["mining", "faucet", "dust"]) - )) - - elif scenario == "privacy_conscious": - for i in range(15): - utxos.append(make_utxo( - random.choice([1_000_000, 2_000_000, 5_000_000, 10_000_000]), - ScriptType.P2TR, "coinjoin" - )) - - return utxos - - -# ============================================================ -# Exercise 1: Largest-First Selection -# ============================================================ - -def select_largest_first(utxos: List[UTXO], target: int, - fee_rate: float = 5.0) -> Optional[CoinSelectionResult]: - """Select UTXOs by picking the largest first. - - Simple and deterministic, but creates obvious change outputs - and links large UTXOs unnecessarily. - - Args: - utxos: available UTXOs - target: amount to send (satoshis) - fee_rate: fee rate in sat/vB - - Returns: - CoinSelectionResult or None if insufficient funds - """ - # YOUR CODE HERE - # - # 1. Sort UTXOs by value, largest first - # 2. Iterate through sorted UTXOs, adding each to the selection - # 3. After each addition, calculate the fee for the current selection - # fee = estimate_tx_fee(len(selected), 2 if change else 1, - # [u.script_type for u in selected], fee_rate) - # 4. Check if total_input >= target + fee - # - If total_input - target - fee < DUST_LIMIT (546 sats): no change needed - # - Otherwise: change_amount = total_input - target - fee - # 5. Return CoinSelectionResult - # - # DUST_LIMIT = 546 # satoshis - - pass - - -# ============================================================ -# Exercise 2: Branch and Bound (Exact Match) -# ============================================================ - -def select_branch_and_bound(utxos: List[UTXO], target: int, - fee_rate: float = 5.0, - max_tries: int = 100_000) -> Optional[CoinSelectionResult]: - """Select UTXOs to exactly match the target (no change output). - - This is Bitcoin Core's preferred algorithm. Finding an exact match - eliminates the change output entirely — great for privacy. - - Args: - utxos: available UTXOs - target: amount to send (satoshis) - fee_rate: fee rate in sat/vB - max_tries: maximum iterations before giving up - - Returns: - CoinSelectionResult with change_amount=0, or None if no exact match found - """ - # YOUR CODE HERE - # - # Branch and bound algorithm: - # - # The "effective value" of a UTXO = value - cost_to_spend_it - # cost_to_spend = (utxo.input_weight / 4) * fee_rate - # - # The "effective target" = target + overhead_fee - # overhead_fee = estimate_tx_fee(0, 1, [], fee_rate) # just the non-input overhead - # - # TOLERANCE = 50 * fee_rate # Allow overpaying up to 50 vbytes worth of fees - # - # Algorithm (depth-first search with backtracking): - # 1. Sort UTXOs by effective value, descending - # 2. Maintain a selection set and current_value = 0 - # 3. For each depth (UTXO index): - # a. TRY INCLUDING this UTXO: - # current_value += effective_value[i] - # If current_value >= effective_target AND - # current_value <= effective_target + TOLERANCE: - # → FOUND exact match! Return it. - # If current_value > effective_target + TOLERANCE: - # → Backtrack (undo inclusion, skip to exclusion branch) - # b. TRY EXCLUDING this UTXO: - # Check if remaining UTXOs can still reach effective_target - # If not → backtrack further - # 4. If max_tries exceeded or no match found → return None - # - # This is a simplified version. The full Bitcoin Core implementation - # is in src/wallet/coinselection.cpp (SelectCoinsBnB). - - pass - - -# ============================================================ -# Exercise 3: Privacy-Optimized Selection -# ============================================================ - -def select_privacy_optimized(utxos: List[UTXO], target: int, - fee_rate: float = 5.0) -> Optional[CoinSelectionResult]: - """Select UTXOs with privacy as the primary objective. - - Privacy rules: - 1. NEVER mix UTXOs from different labels/sources - (e.g., don't combine "exchange" + "coinjoin" UTXOs) - 2. Prefer UTXOs from CoinJoin outputs (already mixed) - 3. Prefer same script type for all inputs (avoid fingerprinting) - 4. Try to avoid change (use branch-and-bound within label groups) - 5. If change is needed, prefer smaller change amounts - - Args: - utxos: available UTXOs - target: amount to send (satoshis) - fee_rate: fee rate in sat/vB - - Returns: - CoinSelectionResult or None if insufficient funds - """ - # YOUR CODE HERE - # - # Strategy: - # 1. Group UTXOs by label - # 2. For each label group (prioritize "coinjoin" > "unknown" > others): - # a. Try branch_and_bound within this group (best: no change!) - # b. If no exact match, try largest_first within this group - # c. If this group alone can cover target + fee, use it - # 3. If no single group suffices: - # a. Fall back to combining groups (privacy degradation!) - # b. Prefer combining groups with the same script type - # c. Log a WARNING: "Privacy degradation: mixing UTXO sources" - # 4. Return the result with the best privacy properties - # - # Scoring (for choosing between valid selections): - # - No change: +50 - # - Single label group: +40 - # - CoinJoin UTXOs: +30 - # - Same script type: +20 - # - Fewer inputs: +10 per input avoided (fewer CIOH links) - - pass - - -# ============================================================ -# Exercise 4: Random Selection (Knapsack-style) -# ============================================================ - -def select_random(utxos: List[UTXO], target: int, - fee_rate: float = 5.0, - rounds: int = 1000) -> Optional[CoinSelectionResult]: - """Select UTXOs randomly, picking the best result from many attempts. - - Adds entropy to defeat deterministic analysis, while still - trying to minimize waste. - - Args: - utxos: available UTXOs - target: amount to send (satoshis) - fee_rate: fee rate in sat/vB - rounds: number of random attempts - - Returns: - CoinSelectionResult with lowest waste score - """ - # YOUR CODE HERE - # - # For each round: - # 1. Shuffle the UTXOs randomly - # 2. Walk through shuffled list, accumulating until target + fee is met - # 3. Calculate the CoinSelectionResult (including waste) - # 4. Keep track of the result with the lowest waste - # - # After all rounds, return the best result - # - # BONUS: Instead of pure random, use a weighted random: - # - UTXOs closer to the target get higher weight - # - This makes exact matches more likely - - pass - - -# ============================================================ -# Exercise 5: Privacy Scoring -# ============================================================ - -def score_privacy(result: CoinSelectionResult, all_utxos: List[UTXO]) -> Dict[str, any]: - """Score the privacy properties of a coin selection result. - - Args: - result: the coin selection result to score - all_utxos: the full UTXO set (for context) - - Returns: - dict with: - 'total_score': int 0-100 - 'breakdown': dict of individual scores - 'warnings': list of privacy warnings - 'recommendations': list of suggestions - """ - # YOUR CODE HERE - # - # Scoring criteria: - # - # 1. CHANGE OUTPUT (0-25 points): - # - No change: 25 points - # - Change < 1% of target: 20 points - # - Change < 10% of target: 10 points - # - Change > 50% of target: 0 points (obvious pattern) - # - # 2. INPUT MIXING (0-25 points): - # - All UTXOs from same label: 25 points - # - 2 labels mixed: 10 points - # - 3+ labels mixed: 0 points - # WARNING: "Mixing UTXOs from {labels} links these identities" - # - # 3. SCRIPT TYPE UNIFORMITY (0-20 points): - # - All inputs same script type: 20 points - # - 2 script types: 10 points - # - 3+ script types: 0 points (obvious wallet migration) - # - # 4. NUMBER OF INPUTS (0-15 points): - # - 1 input: 15 points (minimal CIOH exposure) - # - 2 inputs: 10 points - # - 3+ inputs: 5 points - # - 5+ inputs: 0 points - # - # 5. CHANGE AMOUNT PATTERN (0-15 points): - # - No change: 15 points - # - Non-round change: 10 points - # - Round change amount: 5 points (suspicious pattern) - # - Change matches a common denomination: 0 points - # - # Generate warnings and recommendations based on the analysis - - pass - - -# ============================================================ -# Exercise 6: Comparative Analysis -# ============================================================ - -def run_comparison(utxos: List[UTXO], targets: List[int], fee_rate: float = 5.0): - """Run all coin selection algorithms against multiple targets and compare. - - Prints a comparison table showing fee cost, privacy score, - number of inputs, and whether change was created. - """ - algorithms = { - "Largest First": select_largest_first, - "Branch & Bound": select_branch_and_bound, - "Privacy Optimized": select_privacy_optimized, - "Random (best of 1000)": select_random, - } - - print(f"\n{'='*90}") - print(f"COIN SELECTION COMPARISON — {len(utxos)} UTXOs, fee rate {fee_rate} sat/vB") - print(f"{'='*90}") - - total_balance = sum(u.value for u in utxos) - print(f"Total balance: {total_balance / 100_000_000:.8f} BTC") - print(f"UTXO labels: {', '.join(set(u.label for u in utxos))}") - print(f"Script types: {', '.join(set(u.script_type.value for u in utxos))}") - - for target in targets: - print(f"\n{'─'*90}") - print(f"TARGET: {target / 100_000_000:.8f} BTC ({target:,} sats)") - print(f"{'─'*90}") - print(f"{'Algorithm':<25} {'Inputs':>6} {'Fee':>8} {'Change':>12} {'Waste':>8} {'Privacy':>8}") - print(f"{'─'*25} {'─'*6} {'─'*8} {'─'*12} {'─'*8} {'─'*8}") - - for name, algo in algorithms.items(): - result = algo(utxos, target, fee_rate) - if result is None: - print(f"{name:<25} {'FAILED':>6}") - continue - - privacy = score_privacy(result, utxos) - privacy_score = privacy['total_score'] if privacy else "N/A" - - change_str = f"{result.change_amount:,}" if result.has_change else "none" - print(f"{name:<25} {result.num_inputs:>6} {result.fee:>7}s " - f"{change_str:>12} {result.waste:>7}s {privacy_score:>7}") - - if privacy and privacy.get('warnings'): - for w in privacy['warnings']: - print(f" ⚠ {w}") - - -# ============================================================ -# Run everything -# ============================================================ - -if __name__ == "__main__": - print("=" * 70) - print("Bitcoin Privacy Track — Coin Selection Simulator") - print("Code Orange Dev School | codeorange.dev") - print("=" * 70) - - # Test with different UTXO sets - scenarios = ["typical", "exchange_heavy", "dust_heavy", "privacy_conscious"] - - # Common payment amounts to test - targets = [ - 100_000, # 0.001 BTC — small payment - 1_000_000, # 0.01 BTC — medium payment - 5_000_000, # 0.05 BTC — larger payment - 25_000_000, # 0.25 BTC — significant payment - ] - - for scenario in scenarios: - print(f"\n\n{'#'*70}") - print(f"SCENARIO: {scenario.upper()}") - print(f"{'#'*70}") - - utxos = generate_utxo_set(scenario) - run_comparison(utxos, targets) - - print("\n" + "=" * 70) - print("REFLECTION QUESTIONS:") - print("=" * 70) - print(""" - 1. Which algorithm produced the best privacy scores? Did it also - have the lowest fees? Explain the trade-off you observed. - - 2. In the 'typical' scenario, the privacy-optimized algorithm - refuses to mix labels. What practical problem does this create - for users with many small UTXOs from different sources? - - 3. Branch-and-bound finds no-change solutions but doesn't always - succeed. In which scenarios did it fail most? Why? - - 4. If you were designing a wallet for activists in a hostile regime, - which coin selection algorithm would you use by default? Why? - - 5. BONUS: Implement a 5th algorithm that combines branch-and-bound - with privacy optimization. Can you beat both on their own metrics? - """) - - print("=" * 70) - print("Done! Bring your results to the weekly session.") - print("=" * 70) diff --git a/privacy-track/phase-1-foundations/README.md b/privacy-track/phase-1-foundations/README.md deleted file mode 100644 index 4cd0492..0000000 --- a/privacy-track/phase-1-foundations/README.md +++ /dev/null @@ -1,687 +0,0 @@ -# Bitcoin Privacy Developer Track: Phase 1 - Foundations (Sessions 1-4) - -## Overview - -Phase 1 establishes the technical and threat-modeling foundations for Bitcoin privacy. Developers learn how chain analysis works, understand transaction structure from a privacy lens, master UTXO management, and recognize wallet fingerprints. By the end of Phase 1, participants can analyze their own transactions for privacy leaks and make informed choices about wallet selection. - -**Time Commitment:** 4 weeks, 8-10 hours per session -**Target Audience:** Intermediate developers (familiar with Bitcoin basics and CLI tools) -**Outcome:** Ability to recognize privacy vulnerabilities and implement privacy-preserving UTXO strategies - ---- - -## Session 1: Chain Analysis & Surveillance - -### Real-World Scenario - -**Maria's Privacy Crisis** -Maria runs a small organic coffee shop in Bali. She started accepting Bitcoin in 2023 and promoted her payment address on social media. Within months, a competitor used blockchain explorers to track her revenue: they saw every payment, calculated her sales volume, and could estimate her profit margins. One customer noticed their personal address was linked to Maria's shop address and felt exposed. Maria is now considering closing her Bitcoin payments or finding a private solution. But she still has 1.2 BTC sitting in that address-moving it would broadcast her entire transaction history to chain analysis firms that she suspects are already watching. - -### Learning Objectives - -By the end of Session 1, you will: -- Understand how chain analysis firms link addresses and identify behavioral patterns -- Use industry-standard tools (OXT.me, mempool.space) to perform analysis -- Recognize clustering heuristics (change address identification, transaction timing) -- Evaluate the threat model of different business use cases -- Articulate why privacy is not optional for business operations - -### Privacy Tools You'll Use - -- **OXT.me** - Advanced chain analysis and address clustering tool (Bitcoin-only, no KYC required to use) -- **mempool.space** - Real-time transaction explorer with privacy analysis features -- **Chainalysis Reactor (demo mode)** - Understanding enterprise surveillance capabilities (public demo) -- **bitcoin-cli** - Inspecting raw transaction data -- **Entropy** - Understanding address linking heuristics - -### Hands-On Exercise: "Identify Yourself" - -Your goal: analyze a public Bitcoin address to discover how much you can learn without additional context. - -**Step 1:** Go to https://oxt.me -**Step 2:** Search for address `1A1z7agoat4EvZ2D1V6v6ox6KSQqSWCVH5` (a famous early address) -**Step 3:** Document in a local file: -- How many incoming transactions? -- How many UTXOs were created from this address? -- What does the "change address" pattern tell you? -- Can you identify likely sweep transactions vs. business-use payments? - -**Step 4:** Go to https://mempool.space/address/1A1z7agoat4EvZ2D1V6v6ox6KSQqSWCVH5 -**Step 5:** Compare the data shown in mempool vs OXT: -- What additional metadata does mempool expose (timestamps, fee estimates)? -- How would a law enforcement agency use this to correlate payments across time? - -**Step 6:** Open Terminal and run: -```bash -curl -s https://mempool.space/api/address/1A1z7agoat4EvZ2D1V6v6ox6KSQqSWCVH5 | jq '.chain_stats' -``` -- Examine the JSON: total_tx_count, funded_txo_sum, spent_txo_sum -- Calculate the net movement of funds through this address over time - -**Step 7:** Document a written threat assessment: -- If you were law enforcement, what would you conclude about this address's owner? -- What behavioral patterns indicate business vs. personal use? -- What additional information would you need to identify the owner? - -**Step 8:** Write pseudocode for an automated clustering algorithm: -``` -For each address in a transaction: - IF (output[i] > output[j] for all j != i): - LIKELY_CHANGE = output[i] - CLUSTER(sender_address, all output addresses) -``` - -### Open a PR This Week - -**Contribute to bitcoin-inquisition or mempool.space** - -1. Visit https://github.com/mempool/mempool -2. Navigate to Issues tab, filter by `label: "good first issue"` -3. Pick one of: - - Add a new privacy metric to address analysis (e.g., "change address confidence score") - - Implement a clustering visualization (improve address graph display) - - Write a test case for the Chainalysis-resistant address detection logic -4. Fork the repo, create a branch, submit a PR with clear documentation of the privacy metric or feature -5. Reference this PR in your final Phase 1 submission - -**Alternative:** Contribute to OXT.me documentation: -1. Go to https://github.com/oxt-research/blockchain-analytics-research -2. Add a tutorial or documentation for the clustering heuristics you learned this week -3. Submit a markdown file with 500+ words of analysis and examples - -### Daily Life Privacy Tip - -**Use mempool.space's "Privacy Analysis" feature before every transaction.** - -When you're about to send Bitcoin: go to mempool.space, paste your address, and review the "Cluster" analysis. You'll immediately see which of your previous addresses are linked by chain analysis. Before consolidating UTXOs or making large transfers, check whether you're about to merge outputs that chain analysis has already separated. This 30-second check prevents you from accidentally revealing hidden business relationships. - -**Concrete example:** If you have KYC coins from a CoinBase deposit linked to address A, and non-KYC coins from a peer-to-peer trade linked to address B, do NOT merge them in a single transaction. Chain analysis will immediately correlate your KYC profile with your private activity. Mempool's clustering analysis shows this risk visually. - -### Reading List with Links - -1. **"How Chainalysis Made Me Paranoid" - Unchained Podcast** - https://podcasts.apple.com/us/podcast/how-chainalysis-made-me-paranoid/id1438148082 - - 45-min audio primer on how surveillance firms work - -2. **"Heuristics for Bitcoin Address Clustering" - OXT Research** - https://github.com/oxt-research/blockchain-analytics-research/blob/master/clustering_heuristics.md - - Technical reference for change address identification - -3. **"Chain Analysis Fundamentals" - Bitcoin Privacy Institute** - https://bitcoinprivacyinstitute.org/fundamentals/ - - Interactive tutorial on linking addresses and pattern recognition - -4. **Chainalysis Reactor Public Demo** - https://go.chainalysis.com/demo-reactor.html - - See enterprise surveillance tools in action (no login required) - -5. **"Transaction Fee Analysis: A Window Into Miner Behavior" - Jameson Lopp** - https://blog.lopp.net/transaction-fee-analysis/ - - How fee patterns leak timing information - -6. **OXT.me User Guide** - https://oxt.me/guide - - Complete documentation for the tools you're using - ---- - -## Session 2: Transaction Anatomy for Privacy - -### Real-World Scenario - -**Developer's Dilemma** -You're building a feature in your Bitcoin wallet that shows users whether their transaction is "private." But how do you analyze privacy at the transaction level? You find a transaction in mempool: multiple inputs, multiple outputs, mixed output values. One output is significantly larger-is it change or payment to the merchant? Is the change address on-chain reused? Does the wallet have a time-stamp of when the change address was created? Which heuristic breaks down? A user tests your feature and finds contradictory privacy scores. You realize: you can't evaluate transaction privacy without understanding exactly how that transaction's data structure leaks information. - -### Learning Objectives - -By the end of Session 2, you will: -- Understand the on-chain differences between privacy-leaking and privacy-preserving transactions -- Analyze transaction structure using bitcoin-cli and Sparrow Wallet -- Identify which outputs are likely change vs. payment -- Recognize timing and amount-based privacy leaks -- Design transaction patterns that resist common heuristics - -### Privacy Tools You'll Use - -- **bitcoin-cli** - Raw transaction inspection (`getrawtransaction`, `decoderawtransaction`) -- **Sparrow Wallet** - Transaction inspector with privacy scoring -- **mempool.space** - Visual transaction dissection -- **bitcoin-tx** - Transaction creation and inspection utility -- **mitmproxy** - Observing network-level leaks (optional advanced) - -### Hands-On Exercise: "Decode the Privacy Leak" - -**Step 1:** Set up a local Bitcoin testnet node (or use a public testnet): -```bash -bitcoind -testnet -server -bitcoin-cli -testnet getblockcount -``` - -**Step 2:** Create a transaction with multiple inputs and outputs: -```bash -bitcoin-cli -testnet createrawtransaction \ - '[{"txid":"TXID1","vout":0},{"txid":"TXID2","vout":1}]' \ - '{"address1":0.5,"address2":0.3}' -``` -(Use testnet addresses from a faucet if needed) - -**Step 3:** Sign and broadcast: -```bash -bitcoin-cli -testnet signrawtransactionwithkey '[]' -bitcoin-cli -testnet sendrawtransaction -``` - -**Step 4:** Inspect your own transaction: -```bash -bitcoin-cli -testnet getrawtransaction true -``` - -**Step 5:** Analyze the JSON output. Document: -- Which output is likely change? (Hint: check address reuse, value size, position) -- What does the version field (`vin[0].sequence`) tell you about locktime usage? -- Are you using RBF (Opt-In Replace-By-Fee)? How does this leak timing? - -**Step 6:** Open **Sparrow Wallet** (https://sparrowwallet.com): -- Go to Tools → Transaction and paste your transaction hex -- Read Sparrow's privacy assessment -- Does it agree with your analysis? Where do you disagree? - -**Step 7:** Create a comparison spreadsheet: - -| Transaction Property | Leaks Information? | Why / Why Not | -|---|---|---| -| Number of inputs | ? | More inputs = higher UTXO consolidation | -| Sequence numbers | ? | Indicates RBF capability, locktime | -| Output amounts | ? | Round numbers vs. random amounts | -| Output ordering | ? | Change address patterns | -| Script type (P2PKH vs P2WPKH) | ? | Wallet fingerprinting | - -**Step 8:** Read the Sparrow analysis feature documentation and write pseudocode for a privacy scoring algorithm: -``` -transaction_privacy_score = 100 -IF (num_inputs > 1): - score -= 10 // Input merging reveals owned addresses -IF (output[largest].index == last): - score -= 15 // Change address at end (predictable) -IF (output[i].is_round_number): - score -= 5 // Round numbers = likely payment (not change) -RETURN score -``` - -### Open a PR This Week - -**Contribute to Sparrow Wallet or BTCPay Server** - -1. Visit https://github.com/sparrowwallet/sparrow or https://github.com/btcpayserver/btcpayserver -2. Look for issues tagged `privacy` or `enhancement` -3. Choose one of: - - Improve transaction privacy scoring (add new heuristics) - - Implement fee-amount randomization (reduce timing leaks) - - Add a "Privacy Report" feature that explains each output's role - - Create educational tooltips explaining privacy implications of transaction structure -4. Fork, code, and submit PR with test cases -5. Link your PR in Phase 1 final submission - -**Alternative:** Contribute to Sparrow documentation: -1. Go to https://github.com/sparrowwallet/sparrow/tree/master/docs -2. Write a "Transaction Privacy Analysis Guide" (1000+ words) -3. Include real transaction examples and explanations -4. Submit as a Markdown file PR - -### Daily Life Privacy Tip - -**Check the "Privacy" tab in Sparrow before signing any transaction.** - -Every time you're about to broadcast a payment: -1. In Sparrow, click the transaction you're about to send -2. Click the "Privacy" tab (or hover over the privacy icon) -3. Read the warnings -4. If you see "Change address pattern detected" or "Address reuse," stop and adjust: - - Use a new change address (generate fresh) - - Consider breaking the payment into smaller transactions - - Or accept the privacy leak consciously (sometimes necessary for small payments) - -This 10-second review prevents you from accidentally merging addresses or creating obvious change patterns that chain analysis expects. - -### Reading List with Links - -1. **"The Privacy Implications of Bitcoin Transaction Structure" - Bitcoin Optech** - https://bitcoinops.org/en/newsletters/2022/04/06/#spend-patterns-and-privacy - - Technical deep-dive on input/output analysis - -2. **Sparrow Wallet Privacy Features Documentation** - https://www.sparrowwallet.com/docs/transactions.html - - Complete guide to transaction inspection and privacy scoring - -3. **"UTXO Management: A Practical Guide" - Bitcoin Magazine** - https://bitcoinmagazine.com/articles/utxo-management-a-practical-guide - - Real-world examples of privacy-preserving transaction patterns - -4. **mempool.space Transaction Explorer Guide** - https://mempool.space/docs/faq - - Learn to read transaction data from the block explorer - -5. **"Coin Mixing Explained" - Wasabi Wallet Blog** - https://blog.wasabiwallet.io/coinjoin/ - - Context for why alternative transaction patterns exist - -6. **Bitcoin Core Transaction API Reference** - https://developer.bitcoin.org/reference/rpc/getrawtransaction.html - - Complete reference for decoderawtransaction and related commands - ---- - -## Session 3: UTXO Management & Coin Selection - -### Real-World Scenario - -**The Consolidation Problem** -You've received Bitcoin from three different sources: $100 from a friend, $200 from a CoinBase withdrawal, $150 from a client payment. You're storing them separately because you know that merging them creates a transaction that says "one person owns all three." But now you want to buy a $300 laptop. If you select all three UTXOs for the payment, you've just linked your friend, your exchange, and your business income in a single transaction. If you select only the friend and client coins, you leave a separate CoinBase UTXO that makes you a target for exchange surveillance. There's no perfect move. The answer is understanding **coin selection**: which UTXOs minimize privacy leaks for this specific transaction? - -### Learning Objectives - -By the end of Session 3, you will: -- Understand how coin selection strategies affect privacy -- Use Sparrow Wallet's coin control feature to manually select UTXOs -- Implement Bitcoin Core's coin selection algorithm (BnB, Knapsack, First-Fit) -- Recognize the privacy trade-offs of different selection strategies -- Build a UTXO management strategy for your personal or business Bitcoin use - -### Privacy Tools You'll Use - -- **Sparrow Wallet** - Coin control interface with visual UTXO management -- **Bitcoin Core** - Coin selection algorithms (`selectcoins`, `fundrawtransaction`) -- **BTCPay Server** - UTXO selection for merchant payments -- **OXT.me** - Analyzing historical coin selection patterns -- **bitcoin-cli** - Programmatic coin selection - -### Hands-On Exercise: "Engineer the Optimal Spend" - -**Step 1:** Open Sparrow Wallet and create a test wallet (testnet or mainnet view-only): -- Go to Wallet → Settings → Advanced -- Enable "Coin Control" mode -- You'll now see individual UTXOs instead of a combined balance - -**Step 2:** Simulate three UTXOs (you can use real ones if testing on testnet): -- Create fictional labels: "Friend Gift (0.05 BTC)", "Exchange (0.2 BTC)", "Client Payment (0.15 BTC)" -- Document their addresses and age - -**Step 3:** Scenario: You need to send 0.22 BTC to a merchant. Choose your coin selection: - -**Option A: Select Friend + Client (0.2 BTC)** -- Need to add more to cover 0.22 BTC requirement-force selection of Exchange UTXO too -- Privacy cost: Links all three sources -- Fee benefit: Single transaction, lower total fees - -**Option B: Select only Exchange (0.2 BTC) + Friend (0.05 BTC)** -- Exactly covers 0.22 BTC, minimal change -- Privacy benefit: Keeps client payment separate -- Cost: Merchant learns you've linked exchange + friend - -**Option C: Send two separate transactions** -- First: 0.2 BTC from Exchange -- Second: 0.02 BTC from Client, 0.0 BTC change = exact amount -- Privacy benefit: No linking in a single transaction -- Cost: Double fees (2 transactions instead of 1) - -**Step 4:** In Sparrow, select each UTXO and observe: -``` -Right-click UTXO → "Select for Send" or "Deselect" -``` - -**Step 5:** For each option above, simulate the transaction: -- Check Sparrow's privacy score for the resulting transaction -- Record: fee impact, privacy score, UTXO links created - -**Step 6:** Read the Bitcoin Core documentation: -```bash -bitcoin-cli help selectcoins -bitcoin-cli help fundrawtransaction -``` - -**Step 7:** Understand BnB (Branch-and-Bound) coin selection: -``` -Goal: Select UTXOs that minimize "change" -Algorithm: - 1. Sort UTXOs by size - 2. Try combinations that sum as close as possible to target - 3. Minimize change = less on-chain evidence of coin selection -``` - -Write pseudocode for BnB: -``` -target = payment_amount + estimated_fee -best_combination = None -best_waste = INFINITY - -FOR each combination of UTXOs: - sum = total_of_combination - IF (sum >= target): - waste = sum - target - IF (waste < best_waste): - best_waste = waste - best_combination = combination - -RETURN best_combination -``` - -**Step 8:** Create a UTXO Management Policy spreadsheet for yourself: - -| Source | Address | Amount | Age | Privacy Risk | Selection Rule | -|---|---|---|---|---|---| -| Friend Gift | addr1 | 0.05 | 3 mo | Low | Only merge with other gift sources | -| Exchange (KYC) | addr2 | 0.2 | 6 mo | High | Isolate, minimize merge | -| Client Payment | addr3 | 0.15 | 1 mo | Medium | Separate from Exchange | - -**Rule Set:** "For any spend under 0.1 BTC, use only Friend UTXOs. For larger spends, use Client + Friend if sum is sufficient, only merge with Exchange if unavoidable and spend amount is already de-anonymized." - -### Open a PR This Week - -**Contribute to Bitcoin Core or BTCPay Server** - -1. Visit https://github.com/bitcoin/bitcoin (coin selection) or https://github.com/btcpayserver/btcpayserver (merchant coin selection) -2. Look for coin selection issues or enhancements tagged `privacy` -3. Choose one of: - - Implement a privacy-aware coin selection algorithm (prefer older UTXOs to avoid timing leaks) - - Add documentation to coin selection logic with privacy implications - - Create test cases for coin selection with mixed UTXO ages - - Implement "privacy mode" in BTCPay that minimizes UTXO merging -4. Submit PR with tests and documentation -5. Link in Phase 1 final submission - -**Alternative:** Contribute to Sparrow: -1. Go to https://github.com/sparrowwallet/sparrow -2. File or pick an issue related to coin control UI improvements -3. Implement: "Privacy warning when merging KYC + non-KYC UTXOs" -4. Submit PR - -### Daily Life Privacy Tip - -**Label every UTXO in Sparrow with its source and risk level.** - -In Sparrow Wallet, every UTXO you receive should be immediately labeled: -- "KYC: CoinBase" (high risk, minimize merging) -- "Peer: Alice" (low risk, safe to merge) -- "Inherited: Unknown" (medium risk, keep isolated) - -When you spend, look at your UTXO list and consciously select coins that minimize linking. This takes 30 seconds per transaction and prevents you from accidentally revealing your entire financial graph. - -### Reading List with Links - -1. **"Coin Selection and Wallet Fingerprinting" - Bitcoin Optech** - https://bitcoinops.org/en/newsletters/2022/10/05/#coin-selection-for-privacy - - Technical analysis of coin selection privacy implications - -2. **Bitcoin Core Coin Selection Implementation** - https://github.com/bitcoin/bitcoin/blob/master/src/wallet/coinselection.cpp - - Source code comments explaining the algorithms - -3. **"UTXO Consolidation: When and Why" - Blockstream Blog** - https://blockstream.com/2017/12/12/utxo-consolidation/ - - Strategic guide to managing UTXO sets - -4. **Sparrow Coin Control Guide** - https://www.sparrowwallet.com/docs/wallets.html#coin-control - - Complete visual walkthrough of coin selection - -5. **"Branch-and-Bound Coin Selection" - Bitcoin Core Docs** - https://github.com/bitcoin/bitcoin/blob/master/src/wallet/coinselection.h - - Algorithm documentation and rationale - -6. **BTCPay Server UTXO Selection** - https://docs.btcpayserver.org/features/payouts/#coin-selection - - Merchant-focused coin selection strategies - ---- - -## Session 4: Wallet Fingerprinting - -### Real-World Scenario - -**The Fingerprint That Revealed Everything** -A Bitcoin analyst notices a pattern: transactions from a podcast host always use P2WPKH-nested-in-P2SH outputs (SegWit v0), always have exactly 3 inputs and 2 outputs, and always pay to Blockstream's BTCPay Server address (which is public). The analyst cross-references transaction patterns with podcast sponsors, donation amounts, and announcement timing. Within weeks, they've mapped the host's entire funding structure without ever knowing the hosting wallet's seed phrase. The host thought they were using multiple wallets, but each one had a unique fingerprint. The lesson: **every wallet implementation leaks information through its transaction structure**. Different wallets default to different script types, fee strategies, and output ordering. This is "wallet fingerprinting." - -### Learning Objectives - -By the end of Session 4, you will: -- Recognize wallet fingerprints from transaction patterns -- Understand why different wallets produce different signatures -- Compare privacy characteristics of major wallet implementations -- Choose a wallet based on privacy criteria, not just features -- Analyze a transaction and identify the likely wallet software that created it - -### Privacy Tools You'll Use - -- **Sparrow Wallet** - View-only wallet inspector with script type analysis -- **Electrum** - Check legacy vs. modern output formats -- **Bitcoin Core** - Full node with raw transaction inspection -- **BlueWallet** - Mobile wallet with different fingerprints -- **Cake Wallet** - Privacy-focused mobile option -- **mempool.space** - Visualizing transaction patterns -- **OXT.me** - Clustering and fingerprinting analysis - -### Hands-On Exercise: "Identify the Wallet" - -**Part 1: Understand Script Types (and Why They Matter)** - -Different wallets default to different address types: -- P2PKH (1...) - Legacy, most obvious script type, high fingerprinting risk -- P2SH (3...) - Can hide script type, but SegWit-in-P2SH has unique signatures -- P2WPKH (bc1q...) - Native SegWit, most common modern format -- P2TR (bc1p...) - Taproot, newest, becomes more common yearly - -**Step 1:** Go to mempool.space and find a recent transaction with mixed input/output script types: -- Example: a transaction with both P2PKH and P2WPKH inputs -- Screenshot it and annotate: which input type is legacy? which is modern? - -**Step 2:** Search https://bitcoin.stackexchange.com for "wallet fingerprint" and read 2-3 top answers: -- Understand why Electrum has a recognizable fingerprint -- Note: "coin selection algorithm differences create patterns" - -**Part 2: Hands-On Wallet Comparison** - -**Step 3:** Collect transaction examples from four wallets (use testnet or view transactions from analysis): - -| Wallet | Example Tx | Input Type | Output Type | Fee Strategy | Change Pattern | -|---|---|---|---|---|---| -| Bitcoin Core | ? | ? | ? | ? | ? | -| Sparrow | ? | ? | ? | ? | ? | -| Electrum | ? | ? | ? | ? | ? | -| BlueWallet | ? | ? | ? | ? | ? | - -**Step 4:** Analyze each wallet's characteristics: - -For **Bitcoin Core** transactions: -```bash -bitcoin-cli getrawtransaction true -``` -- Look at `vin[].scriptSig` lengths -- Note `vout[].scriptPubKey.type` values -- Observe fee calculation: do outputs have specific patterns? - -For **Sparrow** transactions: -- Check address descriptor format -- Note: Sparrow can intentionally randomize output order to obscure change -- Check if all inputs are same script type (characteristic fingerprint) - -For **Electrum** transactions: -- Older versions have predictable change address patterns -- Search for Electrum's coin selection algorithm on GitHub -- Note: Electrum's specific fee behavior - -For **BlueWallet / Cake Wallet** transactions: -- Mobile wallets often have less randomization -- Check if transaction structure is consistent wallet-to-wallet - -**Step 5:** Create a fingerprinting checklist: - -``` -WALLET FINGERPRINTING CHECKLIST: - -Transaction observed: - -Input analysis: - [ ] All same script type? (Bitcoin Core often does this) - [ ] Coin selection follows age pattern? (Electrum sometimes does) - [ ] Inputs ordered in specific way? (Some wallets sort, others don't) - -Output analysis: - [ ] Change address is largest output? (Legacy fingerprint) - [ ] Change address is last output? (Common default) - [ ] Outputs in sorted order? (Bech32 sorting indicates Bitcoin Core) - -Fee analysis: - [ ] Is fee round number (0.0001 BTC)? (Indicates simple strategy) - [ ] Fee proportional to size? (Indicates rate-based calculation) - -Script type analysis: - [ ] P2PKH only (very old wallet) - [ ] P2SH mixed (potentially modern Electrum) - [ ] P2WPKH (native SegWit, most common) - [ ] P2TR (Taproot, Bitcoin Core 24+, Sparrow 1.7+) - -Confidence: ___/10 (How confident are you this is [Wallet X]?) -``` - -**Step 6:** Find three real Bitcoin transactions (mainnet, public addresses): -1. One you believe is Bitcoin Core -2. One you believe is Electrum or Sparrow -3. One you believe is a mobile wallet (BlueWallet, Cake, etc.) - -Document your analysis for each. Include: -- Screenshot of the transaction -- Fingerprinting analysis (check list above) -- Your conclusion and confidence level - -**Step 7:** Write a brief "Privacy Ranking" of the four wallets you studied: - -``` -PRIVACY SCORE (1-10): - -Bitcoin Core: ___ - Pros: Can customize everything, randomizes output order - Cons: Large fee variance = timing leak, script type flexibility - Ideal for: Power users who want full control - -Sparrow: ___ - Pros: Excellent coin control, output order randomization, full transparency - Cons: Desktop-only, requires setup - Ideal for: Privacy-conscious developers - -Electrum: ___ - Pros: Established, recoverable seed phrases - Cons: Older versions have predictable patterns, legacy fingerprint - Ideal for: Users who prioritize recovery, willing to accept some fingerprinting - -BlueWallet / Cake: ___ - Pros: Mobile convenience, some support for newer features - Cons: Less control, less randomization, cloud connectivity risks - Ideal for: Mobile-first users, accepting some privacy trade-offs -``` - -### Open a PR This Week - -**Contribute to Wallet Fingerprinting Research or Implementation** - -1. Visit https://github.com/bitcoin/bitcoin (wallet fingerprinting in output sorting) - OR https://github.com/sparrowwallet/sparrow (output randomization features) -2. Pick one of: - - Improve output sorting randomization in Bitcoin Core (reduce fingerprinting) - - Add wallet fingerprinting documentation to Bitcoin Core - - Implement "Anonymity Score" feature in Sparrow that warns of wallet-specific patterns - - Create test cases that verify output randomization is working correctly -3. Research existing PRs: - - Bitcoin Core #24308 (output randomization) - - Sparrow (look for "privacy" or "fingerprint" labels) -4. Submit your PR with clear privacy rationale - -**Alternative:** Contribute to wallet fingerprinting research: -1. Fork https://github.com/bitcoinops/bitcoinops.github.io -2. Add a new entry to the "Wallet Privacy Comparison" section -3. Document real transaction examples for each wallet type -4. Submit PR with anonymized transaction analysis - -### Daily Life Privacy Tip - -**Use Sparrow Wallet and enable output randomization for all transactions.** - -Sparrow Wallet intentionally randomizes the order of transaction outputs to prevent analysts from assuming "last output is change." Additionally: - -1. In Sparrow, go to Preferences → Transactions -2. Enable "Randomize outputs" (if available in your version) -3. Set script type preference to the newest you support (P2WPKH or P2TR) -4. For critical privacy transactions, use Bitcoin Core directly with `bitcoind` so you control every detail - -This prevents your wallet software from creating a recognizable fingerprint that ties together your transactions over time. - -### Reading List with Links - -1. **"Wallet Fingerprinting: Identifying Software from Blockchain Transactions"** - https://en.bitcoin.it/wiki/Weaknesses#Wallet_fingerprinting - - Bitcoin wiki reference on fingerprinting techniques - -2. **Bitcoin Core Output Sorting (BIP69)** - https://github.com/bitcoin/bitcoin/pull/24308 - - Discussion of output randomization in Bitcoin Core - -3. **"The Privacy Implications of Wallet Software" - Bitcoin Optech** - https://bitcoinops.org/en/newsletters/2022/06/15/#analyzing-wallet-privacy - - Analysis of different wallet implementations - -4. **Sparrow Wallet Privacy Documentation** - https://www.sparrowwallet.com/docs/privacy.html - - Complete guide to Sparrow's privacy features - -5. **Electrum Blockchain Transaction Patterns** - https://github.com/spesmilo/electrum/blob/master/electrum/coinselection.py - - Source code for Electrum's coin selection (affects fingerprinting) - -6. **"Linkability Attacks in Bitcoin" - Monero Research Lab** - https://www.monerooutreach.org/ - - Cross-chain analysis techniques relevant to wallet fingerprinting - -7. **BlueWallet and Cake Wallet Comparison** - https://www.sparrowwallet.com/docs/wallets.html#wallet-comparison - - Feature and privacy matrix comparing wallet implementations - ---- - -## Phase 1 Capstone Project - -### Your Mission - -Submit a **privacy audit** of your own Bitcoin wallet or a publicly documented transaction: - -1. **Identify the UTXO sources** (KYC exchange, peer-to-peer trade, inheritance, etc.) -2. **Analyze the transaction structure** (inputs, outputs, script types, fees) -3. **Determine the likely wallet** (fingerprinting analysis) -4. **Rate the privacy leaks** (1-10 scale, with justification) -5. **Propose improvements** (alternative transaction structure, wallet selection, coin selection strategy) -6. **Submit as a GitHub PR** to the Bitcoin Privacy Institute curriculum repo - -### Deliverables - -- [ ] Write a 1500+ word privacy audit document -- [ ] Include at least 5 screenshots (transactions, analysis tools, fingerprinting evidence) -- [ ] Reference Session 1-4 concepts explicitly -- [ ] Propose 3 specific changes to improve privacy -- [ ] Create a PR to a Bitcoin wallet or analysis tool repository (link in your submission) -- [ ] Be honest: if there are privacy leaks in your own wallet, acknowledge them and explain why you accept the trade-off - ---- - -## Getting Help - -- **Bitcoin Privacy Institute Discord:** [link] -- **Bitcoin Optech Newsletter:** https://bitcoinops.org/en/newsletters/ -- **Sparrow Wallet Documentation:** https://www.sparrowwallet.com/docs/ -- **Bitcoin Core RPC Reference:** https://developer.bitcoin.org/reference/rpc/ -- **OXT.me Tutorials:** https://oxt.me/guide - ---- - -## What's Next? - -After completing Phase 1 (Foundations), advance to **Phase 2: Silent Payments** where you'll implement BIP352 senders, understand scanning algorithms, and contribute to production Bitcoin privacy infrastructure. - -**Phase 2 Preview:** Building a Silent Payments address generator, implementing the sender protocol, and deploying privacy solutions for real-world use cases (donations, merchant payments, client invoicing). diff --git a/privacy-track/phase-1-foundations/session-04-wallet-fingerprinting/wallet_fingerprint_lab.py b/privacy-track/phase-1-foundations/session-04-wallet-fingerprinting/wallet_fingerprint_lab.py deleted file mode 100644 index e259c77..0000000 --- a/privacy-track/phase-1-foundations/session-04-wallet-fingerprinting/wallet_fingerprint_lab.py +++ /dev/null @@ -1,455 +0,0 @@ -""" -Wallet Fingerprinting Exercise -=============================== - -This exercise teaches students how to identify which wallet software created a Bitcoin transaction -by analyzing characteristic patterns and heuristics. Real-world wallets often have distinctive -behaviors that can leak information about the user, even without identifying personal information. - -Learning Objectives: -- Understand wallet fingerprinting and privacy implications -- Identify common fingerprinting vectors: locktime, sequence numbers, script types, fee patterns -- Implement heuristic matching to guess wallet software -- Learn how to construct privacy-aware transactions - -Students must implement seven analysis functions and one construction function to complete this lab. -""" - -from typing import Dict, List, Set, Tuple -from dataclasses import dataclass - - -@dataclass -class Transaction: - """Represents a simplified Bitcoin transaction for analysis.""" - version: int - locktime: int - inputs: List[Dict] # Each has: sequence, script_type, amount - outputs: List[Dict] # Each has: script_type, amount - fee_rate: float # satoshis per byte - - -# Sample transaction data representing different wallets -SAMPLE_TRANSACTIONS = [ - # TX 1: Bitcoin Core - anti-fee-sniping, max sequence, P2WPKH outputs - Transaction( - version=2, - locktime=856000, # Anti-fee-sniping: recent block height - inputs=[ - {"sequence": 0xfffffffe, "script_type": "P2WPKH", "amount": 50000}, - {"sequence": 0xfffffffe, "script_type": "P2WPKH", "amount": 30000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 45000}, - {"script_type": "P2WPKH", "amount": 34500}, - ], - fee_rate=2.5, - ), - # TX 2: Electrum - always zero locktime, RBF signaling - Transaction( - version=2, - locktime=0, - inputs=[ - {"sequence": 0xfffffffd, "script_type": "P2PKH", "amount": 100000}, - ], - outputs=[ - {"script_type": "P2PKH", "amount": 95000}, - ], - fee_rate=1.8, - ), - # TX 3: BlueWallet - random locktime, mixed script types - Transaction( - version=2, - locktime=712345, - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2WPKH", "amount": 75000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 50000}, - {"script_type": "P2WSH", "amount": 24500}, - ], - fee_rate=3.2, - ), - # TX 4: Wasabi - all-max sequence, multiple outputs, round change - Transaction( - version=2, - locktime=0, - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2WPKH", "amount": 100000}, - {"sequence": 0xffffffff, "script_type": "P2WPKH", "amount": 50000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 100000}, - {"script_type": "P2WPKH", "amount": 50000}, - ], - fee_rate=2.0, - ), - # TX 5: Sparrow - structured output ordering, precise fee - Transaction( - version=2, - locktime=0, - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2WPKH", "amount": 200000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 100000}, - {"script_type": "P2WPKH", "amount": 99567}, - ], - fee_rate=4.33, - ), - # TX 6: Samourai - multiple inputs, complex output set, suspicious ordering - Transaction( - version=2, - locktime=0, - inputs=[ - {"sequence": 0xfffffffe, "script_type": "P2WPKH", "amount": 50000}, - {"sequence": 0xfffffffe, "script_type": "P2WPKH", "amount": 40000}, - {"sequence": 0xfffffffe, "script_type": "P2WPKH", "amount": 30000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 45000}, - {"script_type": "P2WPKH", "amount": 35000}, - {"script_type": "P2WPKH", "amount": 39250}, - ], - fee_rate=2.75, - ), - # TX 7: Trezor Suite - consistent sequence, P2WPKH preference - Transaction( - version=2, - locktime=0, - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2WPKH", "amount": 80000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 75000}, - {"script_type": "P2WPKH", "amount": 4800}, - ], - fee_rate=0.5, - ), - # TX 8: Ledger Live - round amounts, consistent approach - Transaction( - version=2, - locktime=0, - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2WPKH", "amount": 100000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 50000}, - {"script_type": "P2WPKH", "amount": 50000}, - ], - fee_rate=1.0, - ), - # TX 9: Blockstream Green - segwit native, specific patterns - Transaction( - version=2, - locktime=0, - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2WSH", "amount": 150000}, - ], - outputs=[ - {"script_type": "P2WSH", "amount": 145000}, - ], - fee_rate=0.8, - ), - # TX 10: Unknown wallet - mixed signals - Transaction( - version=2, - locktime=567890, - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2PKH", "amount": 60000}, - {"sequence": 0xfffffffd, "script_type": "P2WPKH", "amount": 40000}, - ], - outputs=[ - {"script_type": "P2PKH", "amount": 50000}, - {"script_type": "P2WPKH", "amount": 48500}, - ], - fee_rate=2.1, - ), -] - - -# Known wallet fingerprints - characteristics of different wallet implementations -WALLET_FINGERPRINTS = { - "Bitcoin Core": { - "locktime_pattern": "anti-fee-sniping", - "sequence_pattern": "all-max-or-rbf", - "script_types": {"P2WPKH", "P2WSH"}, - "rbf_signaling": True, - "output_ordering": "default", - }, - "Electrum": { - "locktime_pattern": "always-zero", - "sequence_pattern": "rbf-default", - "script_types": {"P2PKH", "P2WPKH"}, - "rbf_signaling": True, - "output_ordering": "default", - }, - "BlueWallet": { - "locktime_pattern": "random", - "sequence_pattern": "all-max", - "script_types": {"P2WPKH", "P2WSH"}, - "rbf_signaling": False, - "output_ordering": "default", - }, - "Wasabi": { - "locktime_pattern": "always-zero", - "sequence_pattern": "all-max", - "script_types": {"P2WPKH"}, - "rbf_signaling": False, - "output_ordering": "deterministic", - }, - "Sparrow": { - "locktime_pattern": "always-zero", - "sequence_pattern": "all-max", - "script_types": {"P2WPKH", "P2WSH"}, - "rbf_signaling": False, - "output_ordering": "bip69-ordered", - }, - "Samourai": { - "locktime_pattern": "always-zero", - "sequence_pattern": "rbf-default", - "script_types": {"P2WPKH"}, - "rbf_signaling": True, - "output_ordering": "shuffled", - }, - "Trezor Suite": { - "locktime_pattern": "always-zero", - "sequence_pattern": "all-max", - "script_types": {"P2WPKH"}, - "rbf_signaling": False, - "output_ordering": "default", - }, - "Ledger Live": { - "locktime_pattern": "always-zero", - "sequence_pattern": "all-max", - "script_types": {"P2WPKH"}, - "rbf_signaling": False, - "output_ordering": "default", - }, - "Blockstream Green": { - "locktime_pattern": "always-zero", - "sequence_pattern": "all-max", - "script_types": {"P2WSH"}, - "rbf_signaling": False, - "output_ordering": "default", - }, -} - - -def identify_locktime_pattern(tx: Transaction) -> str: - """ - Analyze the locktime field to identify wallet patterns. - - Locktime patterns: - - "anti-fee-sniping": locktime is recent block height (suggests Bitcoin Core) - - "always-zero": locktime is 0 (many mobile/exchange wallets) - - "random": locktime appears arbitrary - - "unknown": cannot be classified - - Args: - tx: Transaction to analyze - - Returns: - String describing the locktime pattern - """ - # TODO: Implement locktime pattern identification - # Hint: Check if locktime is 0, a recent block height, or random - # Recent blocks would be values > 500,000,000 (block heights) - pass - - -def identify_sequence_pattern(tx: Transaction) -> str: - """ - Analyze input sequence numbers to identify wallet behavior. - - Sequence patterns: - - "all-max": All inputs have sequence 0xffffffff (no RBF, typical hardware wallets) - - "rbf-signaling": Some inputs have sequence < 0xfffffffe (RBF enabled) - - "rbf-default": Mix of sequences suggesting RBF awareness - - "unknown": Cannot be classified - - Args: - tx: Transaction to analyze - - Returns: - String describing the sequence pattern - """ - # TODO: Implement sequence pattern identification - # Hint: 0xffffffff = 4294967295, 0xfffffffe = 4294967294 - # Check all input sequences to determine pattern - pass - - -def identify_script_types(tx: Transaction) -> Set[str]: - """ - Identify all script types used in transaction inputs and outputs. - - This helps identify wallet capabilities and preferences. - Common types: P2PKH, P2WPKH, P2WSH, P2SH, P2TR - - Args: - tx: Transaction to analyze - - Returns: - Set of script types found in inputs and outputs - """ - # TODO: Implement script type identification - # Hint: Collect script types from all inputs and outputs - pass - - -def check_output_ordering(tx: Transaction) -> str: - """ - Analyze output ordering to detect wallet-specific patterns. - - Patterns: - - "bip69-ordered": Outputs appear sorted (BIP69 lexicographic ordering) - - "deterministic": Consistent but non-standard ordering - - "shuffled": Randomized order (privacy-aware) - - "default": Standard/default ordering - - Args: - tx: Transaction to analyze - - Returns: - String describing the output ordering pattern - """ - # TODO: Implement output ordering detection - # Hint: Check if outputs are sorted by script, amount, or appear randomized - pass - - -def estimate_fee_pattern(tx: Transaction) -> str: - """ - Identify fee rate patterns that can fingerprint wallets. - - Patterns: - - "round": Fee rate is a round number (1, 2, 5, 10 sat/byte) - - "precise": Fee rate is precise/unusual (3.33, 4.47, etc.) - - "very-low": Extremely low fee rate (<0.5 sat/byte) - - "unknown": Cannot be classified - - Args: - tx: Transaction to analyze - - Returns: - String describing the fee pattern - """ - # TODO: Implement fee pattern identification - # Hint: Check if fee_rate is a whole number, very low, or has decimals - pass - - -def identify_wallet(tx: Transaction) -> Tuple[str, float]: - """ - Use all heuristics to identify which wallet likely created this transaction. - - This is the main analysis function that combines all fingerprinting signals. - - Args: - tx: Transaction to analyze - - Returns: - Tuple of (wallet_name, confidence_score) - confidence_score ranges from 0.0 to 1.0 - """ - # TODO: Implement wallet identification - # Hint: Call the other analysis functions and compare results to WALLET_FINGERPRINTS - # Score matches and return the best match with confidence - pass - - -def construct_privacy_clean_tx(inputs: List[Dict], outputs: List[Dict]) -> Transaction: - """ - Construct a transaction that minimizes fingerprinting vectors. - - This is the defensive counterpart to wallet fingerprinting - if you're trying - to avoid identification, follow these principles: - - Use deterministic script types only - - Randomize sequence numbers appropriately - - Set appropriate locktime - - Use non-standard fee rates - - Shuffle output ordering - - Args: - inputs: List of input specifications - outputs: List of output specifications - - Returns: - A Transaction object designed to minimize fingerprinting - """ - # TODO: Implement privacy-conscious transaction construction - # Hint: Create a transaction that doesn't match known wallet patterns - pass - - -def main(): - """ - Main analysis function - runs the complete fingerprinting analysis on all samples. - """ - print("=" * 70) - print("BITCOIN WALLET FINGERPRINTING LAB") - print("=" * 70) - print() - - for i, tx in enumerate(SAMPLE_TRANSACTIONS, 1): - print(f"Transaction {i} Analysis:") - print("-" * 70) - - # Analyze locktime pattern - locktime_pat = identify_locktime_pattern(tx) - print(f" Locktime Pattern: {locktime_pat} (locktime={tx.locktime})") - - # Analyze sequence pattern - sequence_pat = identify_sequence_pattern(tx) - print(f" Sequence Pattern: {sequence_pat}") - - # Identify script types - script_types = identify_script_types(tx) - print(f" Script Types Used: {script_types}") - - # Check output ordering - output_order = check_output_ordering(tx) - print(f" Output Ordering: {output_order}") - - # Estimate fee pattern - fee_pat = estimate_fee_pattern(tx) - print(f" Fee Pattern: {fee_pat} ({tx.fee_rate} sat/byte)") - - # Identify wallet with confidence - wallet_guess, confidence = identify_wallet(tx) - print(f" Wallet Guess: {wallet_guess} (confidence: {confidence:.1%})") - print() - - print("=" * 70) - print("PRIVACY-AWARE TRANSACTION CONSTRUCTION") - print("=" * 70) - print() - - # Example: Construct a privacy-clean transaction - clean_tx = construct_privacy_clean_tx( - inputs=[ - {"sequence": 0xffffffff, "script_type": "P2WPKH", "amount": 100000}, - ], - outputs=[ - {"script_type": "P2WPKH", "amount": 50000}, - {"script_type": "P2WPKH", "amount": 49500}, - ], - ) - - if clean_tx: - print("Constructed Privacy-Clean Transaction:") - print(f" Locktime: {clean_tx.locktime}") - print(f" Input Sequences: {[i['sequence'] for i in clean_tx.inputs]}") - print(f" Output Script Types: {[o['script_type'] for o in clean_tx.outputs]}") - print(f" Fee Rate: {clean_tx.fee_rate} sat/byte") - print() - - # Try to fingerprint your own transaction - wallet_guess, confidence = identify_wallet(clean_tx) - print(f" Fingerprinting Result: {wallet_guess} ({confidence:.1%} confidence)") - print(" (Good privacy-aware construction should have low confidence)") - - -if __name__ == "__main__": - main() diff --git a/privacy-track/phase-2-silent-payments/README.md b/privacy-track/phase-2-silent-payments/README.md deleted file mode 100644 index f09c8b2..0000000 --- a/privacy-track/phase-2-silent-payments/README.md +++ /dev/null @@ -1,1218 +0,0 @@ -# Bitcoin Privacy Developer Track: Phase 2 - Silent Payments (Sessions 5-8) - -## Overview - -Phase 2 dives deep into **BIP352 Silent Payments**, the next-generation Bitcoin privacy solution. Unlike traditional static payment addresses (which create a privacy leak when reused), Silent Payments allow a sender to generate a unique on-chain output for every payment to the same static address-without on-chain interaction, without per-payment address derivation, and without revealing to observers that multiple payments went to the same recipient. By the end of Phase 2, you'll implement a complete Silent Payments sender, understand scanning and light client architecture, and contribute to production Bitcoin privacy infrastructure. - -**Time Commitment:** 4 weeks, 10-12 hours per session -**Target Audience:** Developers with Phase 1 foundation; crypto/cryptography knowledge helpful -**Prerequisites:** Complete Phase 1 or equivalent knowledge of chain analysis, UTXO management, transaction structure -**Outcome:** Functional Silent Payments implementation, production-ready contributions to Bitcoin Core and wallets - ---- - -## Session 5: How Silent Payments Work - -### Real-World Scenario - -**The Donation Address That Wasn't** -A Bitcoin educator and podcast host publishes a single static Bitcoin address in her podcast bio and social media: `sp1qqgste6gj5mxqt24dxy2r2rkv7pa3qy9f2mqkp89q0wvxc2yujsydvu7jmj` (Silent Payments address). Her listeners donate regularly. A chain analyst looks at the address and sees... nothing. No historical transactions. No UTXOs. Why? Every donation created a **unique, unrelated on-chain output**. The analyst cannot determine: -- How many donors she has -- The total amount received -- Which transactions belong to her -- Whether donors are recurring or one-time - -Meanwhile, the host holds a single private key that can find and spend all incoming Silent Payments without revealing her identity to anyone-not even the donors who funded her. This is Silent Payments: the first practical on-chain privacy solution that solves the "static address reuse" problem without requiring interaction per payment. - -### Learning Objectives - -By the end of Session 5, you will: -- Understand the fundamental problem Silent Payments solves (address reuse privacy leak) -- Grasp the cryptographic primitives: Elliptic Curve Diffie-Hellman, hash-based derivation, tweaking -- Understand the sender-side protocol: how to derive a unique output for every SP address -- Understand the receiver-side protocol: how to scan and claim Silent Payments -- Recognize real-world use cases where Silent Payments are the optimal solution -- Know the status of Silent Payments implementation across wallets and Bitcoin Core - -### Privacy Tools You'll Use - -- **BIP352 Specification** (https://github.com/bitcoin/bips/blob/master/bip-0352.md) -- **Cake Wallet** - First production Silent Payments wallet (already shipping SP support) -- **Bitcoin Core PR #28122** - In-progress Silent Payments implementation -- **Sparrow Wallet** - Upcoming Silent Payments support (planned) -- **Kyoto / Floresta** - Light client scanning infrastructure for SP -- **rust-silentpayments** - Reference implementation (github.com/cygnet3/rust-silentpayments) - -### Hands-On Exercise: "Follow the Silent Payment" - -**Part 1: Understand the Math (Conceptually)** - -**Step 1:** Read BIP352 Section 2 (Protocol Description): -https://github.com/bitcoin/bips/blob/master/bip-0352.md#protocol - -You don't need to memorize the math yet, but understand these concepts: -- **Sender side:** "For each payment to SP address, derive a unique output address" -- **Receiver side:** "Use your private key to scan the blockchain and find all outputs intended for you" -- **Key concept:** ECDH (Elliptic Curve Diffie-Hellman) lets the sender create outputs only the receiver can spend - -**Step 2:** Compare to traditional Bitcoin: -- **Traditional:** Alice publishes address `1Alice...`. Bob sends to it. Carol sends to it. Chain analyst: "Alice received 2 payments." -- **Silent Payments:** Alice publishes SP address `sp1qqAlice...`. Bob sends to it → output is `4f3a...` (unique). Carol sends to it → output is `7e2b...` (unique). Chain analyst: "I see two unrelated payments; I don't know they're both for Alice." - -**Step 3:** Document in your own words: -- Why is address reuse a privacy problem? -- How do Silent Payments prevent address reuse? -- What's the trade-off? (Scanning requires checking every transaction; light clients need special infrastructure) - -**Part 2: Interact with a Real Silent Payments Wallet** - -**Step 4:** Install **Cake Wallet** (free, open-source): -- iOS: App Store or https://cakewallet.com/ -- Android: Play Store or https://cakewallet.com/ -- Desktop (beta): https://github.com/cake-tech/cake_wallet - -**Step 5:** Create a new Bitcoin wallet in Cake Wallet: -- Go to Settings → Add Wallet → Bitcoin -- Choose "Bitcoin" as the coin -- Create new wallet -- Go to Receive -- You'll see TWO address types: - - Regular address (starts with `1`, `3`, or `bc1`) - - **Silent Payments address** (starts with `sp1`) - -**Step 6:** Export and document your Silent Payments address: -``` -Your SP Address: sp1qq______________________ -Receiver key (from wallet): _______________ -``` - -**Step 7:** Use https://oxt.me or mempool.space to search for your SP address: -- Result: Address not found (because SP addresses have no on-chain history until you receive a payment) -- This is normal and expected -- Create a comparison: traditional addresses show transaction history immediately; SP addresses show nothing - -**Step 8:** If you have testnet Bitcoin: -- Send a small amount to your Cake Wallet SP address using a testnet sender (or another wallet that supports SP testnet) -- Watch Cake Wallet scan and detect the payment -- Document: How long did scanning take? Did the payment appear in "Pending"? -- Once confirmed, sweep the payment back to verify you can spend Silent Payments - -**Part 3: Understand the Scanning Problem** - -**Step 9:** Read about Silent Payments scanning: -https://github.com/bitcoin/bips/blob/master/bip-0352.md#scanning - -Key question: How does a receiver know which transactions are theirs? -- Answer: They scan every transaction, test if it contains an output for them -- Problem: Scanning thousands of transactions is slow on mobile -- Solution: Light clients like Kyoto use filters to skip irrelevant transactions - -**Step 10:** Document the scanning trade-off: -``` -SCANNING TRADE-OFF: - -Privacy benefit: -- Static address never appears on-chain -- No address clustering possible -- No way to determine how many payments received - -Privacy/performance trade-off: -- Receiver must scan ALL transactions (or use filtered light client) -- Wallet sees every transaction in Bitcoin (though doesn't download full data) -- More CPU usage than traditional wallets - -Mitigation strategies: -- Use Cake Wallet's built-in SP support (handles scanning for you) -- Use light client with server-based filtering (Kyoto, Floresta) -- Use full node with bitcoin-cli (maximum privacy, maximum resource usage) -``` - -**Step 11:** Compare scanning approaches: - -| Approach | Privacy | Performance | Practicality | -|---|---|---|---| -| Full node scanning | Perfect | Slow | High setup, runs locally | -| Light client (server filter) | Very good | Fast | Relies on server not tracking you | -| Cake Wallet (built-in) | Excellent | Medium | Ready to use, tested | -| Mobile (future) | ? | TBD | Still experimental | - -### Open a PR This Week - -**Contribute to Silent Payments Documentation or Wallets** - -1. Visit https://github.com/bitcoin/bitcoin/pulls (search "Silent Payments" or "BIP352") - OR https://github.com/cake-tech/cake_wallet -2. Choose one of: - - Add documentation to Bitcoin Core PR #28122 explaining the sender/receiver protocol - - Write a user guide for Cake Wallet's Silent Payments feature - - Create test vectors or test cases for BIP352 compliance - - Document the scanning optimization techniques in a GitHub wiki -3. Submit as a PR with clear explanation -4. Link in your Phase 2 submission - -**Alternative:** Contribute to Educational Content -1. Go to https://github.com/bitcoinops/bitcoinops.github.io -2. Add a new Mastery Page or FAQ entry explaining: - - "Why Silent Payments?" - - "How to generate a Silent Payments address" - - "Why scanning matters" -3. Submit PR with at least 500 words - -### Daily Life Privacy Tip - -**Generate your first Silent Payments address today.** - -If you use Cake Wallet, you already have a Silent Payments address. If not: - -1. Download Cake Wallet (testnet or mainnet) -2. Create a Bitcoin wallet -3. Go to Receive and copy your SP address (starts with `sp1`) -4. Share this address publicly (social media, website, podcast) -5. It will never appear on a blockchain explorer -6. You can receive unlimited payments to one static address -7. No one can see your total balance or payment count - -This single address replaces the need to generate new addresses for every donation, invoice, or payment-one of the most impactful privacy improvements in Bitcoin since SegWit. - -### Reading List with Links - -1. **BIP352: Silent Payments** - https://github.com/bitcoin/bips/blob/master/bip-0352.md - - Complete specification (Sections 1-3 are essential) - -2. **"Silent Payments Explained" - Bitcoin Optech** - https://bitcoinops.org/en/newsletters/2022/08/03/#overview-of-the-bip352-silent-payments-proposal - - Non-technical overview of the proposal - -3. **"How Silent Payments Will Change Bitcoin Privacy" - Bitcoin Magazine** - https://bitcoinmagazine.com/articles/silent-payments - - Real-world use case analysis - -4. **Cake Wallet Silent Payments Guide** - https://cakewallet.com/articles/what-are-silent-payments - - User-friendly walkthrough - -5. **Bitcoin Core PR #28122 Discussion** - https://github.com/bitcoin/bitcoin/pull/28122 - - Implementation progress and technical discussion - -6. **Kyoto: Light Client for Silent Payments** - https://github.com/rustaceanrob/kyoto - - Scanning optimization for mobile wallets - -7. **"The Privacy Case for Silent Payments" - Bitcoin Privacy Institute** - https://bitcoinprivacyinstitute.org/silent-payments/ - - Comparison to other privacy solutions (CoinJoin, L2 solutions) - ---- - -## Session 6: Implement SP Sender in Python - -### Real-World Scenario - -**The Invoice Dilemma** -You're building a freelancing platform where clients need to pay creators with Bitcoin. Traditionally, you'd generate a new address for each invoice-but now your creator has hundreds of invoices scattered across the blockchain. Or they could publish one address... but then anyone can see their total income. Silent Payments solves this: the creator publishes one SP address. Each client's payment creates a unique output. The creator receives all payments with a single private key. You, as the platform builder, implement this by writing a **Silent Payments sender**: code that takes a creator's SP address and produces a unique output for that creator's payment. - -### Learning Objectives - -By the end of Session 6, you will: -- Understand the BIP352 sender algorithm in detail -- Implement a complete SP sender in Python (or your preferred language) -- Know how to validate a Silent Payments address -- Know how to use shared secrets and tweaking in practice -- Write test cases against BIP352 test vectors -- Understand the difference between testnet and mainnet SP addresses - -### Privacy Tools You'll Use - -- **Python 3.8+** with `hashlib`, `ecdsa` -- **BIP352 Test Vectors** (github.com/bitcoin/bips/blob/master/bip-0352/vectors.json) -- **bitcoinlib-js** or **libsecp256k1** (for ECDH operations) -- **Sparrow Wallet** or **Bitcoin Core** (to inspect generated transactions) -- **rust-silentpayments** (reference implementation to compare) - -### Hands-On Exercise: "Build a Silent Payments Sender" - -**Step 1:** Set up your Python environment: -```bash -python3 -m venv sp-env -source sp-env/bin/activate -pip install hashlib ecdsa requests -``` - -**Step 2:** Download BIP352 test vectors: -```bash -curl https://raw.githubusercontent.com/bitcoin/bips/master/bip-0352/vectors.json > vectors.json -``` - -**Step 3:** Create a file `sp_sender.py` and start with the address validation: -```python -import hashlib -import json -from typing import Tuple - -class SilentPaymentsSender: - """ - BIP352 Silent Payments Sender Implementation - - This implementation derives unique output addresses for a given - Silent Payments address. - """ - - def __init__(self): - # You'll add ECDH and hashing functions here - pass - - def validate_sp_address(self, sp_address: str) -> bool: - """ - Validate that a string is a valid Silent Payments address. - - SP addresses: - - Start with 'sp1' (mainnet) or 'sp1' (testnet, same prefix) - - Are base32 encoded - - Have a specific format and checksum - """ - # Implement Bech32m decoding and checksum validation - # Reference: BIP350 (Bech32m encoding) - pass - - def decode_sp_address(self, sp_address: str) -> Tuple[bytes, bytes]: - """ - Decode a Silent Payments address into its components. - - Returns: - - (scan_key: bytes, spend_key: bytes) - - These are the public keys that allow deriving unique outputs. - """ - pass - - def derive_output_public_key( - self, - sp_address: str, - sender_private_key: bytes, - m: int # Index (first, second, third payment, etc.) - ) -> bytes: - """ - Derive a unique output public key for a payment. - - Algorithm (BIP352 Section 2): - 1. Compute shared secret using ECDH with sender's private key - and the receiver's scan public key - 2. Hash the shared secret with transaction inputs and output index - 3. Tweak the receiver's spend public key with the hash - 4. Result: unique public key for this payment - - This is the "Q" value in BIP352 terminology. - """ - pass -``` - -**Step 4:** Implement ECDH (Elliptic Curve Diffie-Hellman): -```python -def ecdh(private_key: bytes, public_key: bytes) -> bytes: - """ - Compute ECDH shared secret. - - This uses libsecp256k1 or ecdsa library. - Returns: 32-byte shared secret - """ - # Use ecdsa library or libsecp256k1 - # shared_secret = private_key * public_key (elliptic curve multiplication) - pass -``` - -**Step 5:** Implement hash-based tweaking: -```python -def hash_tweak_add(data: bytes, point: bytes) -> bytes: - """ - Hash data and add the result to a point (Taproot-style tweaking). - - Algorithm: - 1. h = SHA256(data) - 2. Q = point + h*G (elliptic curve point addition) - - Where: - - h is interpreted as a scalar (256-bit number) - - G is the elliptic curve generator point - - Q is the new tweaked point - """ - pass -``` - -**Step 6:** Load BIP352 test vectors: -```python -with open('vectors.json') as f: - test_vectors = json.load(f) - -# Test vectors contain: -# - Sender private keys -# - Silent Payments addresses -# - Expected outputs -# Use these to validate your implementation -``` - -**Step 7:** Test your implementation against test vectors: -```python -def test_sp_sender(): - sender = SilentPaymentsSender() - - for vector in test_vectors['vectors']: - sp_address = vector['address'] - sender_privkey = bytes.fromhex(vector['privkey']) - - for output_idx, expected_output in enumerate(vector['outputs']): - derived_key = sender.derive_output_public_key( - sp_address, - sender_privkey, - output_idx - ) - - assert derived_key.hex() == expected_output, \ - f"Output {output_idx} mismatch" - - print("All test vectors passed!") -``` - -**Step 8:** Create a complete example transaction: -```python -def create_sp_payment(sp_address: str, amount_sat: int, - sender_private_key: bytes) -> dict: - """ - Create a complete Silent Payments payment. - - Returns: - { - 'address': sp_address, - 'output_public_key': derived_public_key, - 'amount_sat': amount_sat, - 'output_script': p2tr_script_pubkey # P2TR script - } - """ - output_key = derive_output_public_key(sp_address, sender_private_key, 0) - - # Convert to Taproot (P2TR) script format - script = create_p2tr_script(output_key) - - return { - 'address': sp_address, - 'output_key': output_key.hex(), - 'amount_sat': amount_sat, - 'script': script.hex() - } -``` - -**Step 9:** Document your implementation: -``` -IMPLEMENTATION SUMMARY: - -Silent Payments Sender Requirements: -- Input: SP address, sender private key -- Output: Unique public key for each payment -- Privacy guarantee: Each output is unique and cannot be linked - -Cryptographic operations used: -1. ECDH (Elliptic Curve Diffie-Hellman) -2. SHA256 hashing -3. Elliptic curve point addition (Taproot-style tweaking) - -Validation: -- Test vectors: PASS -- Mainnet addresses: Ready -- Testnet addresses: Ready -``` - -### Open a PR This Week - -**Contribute to Silent Payments Implementation** - -1. Visit https://github.com/cygnet3/rust-silentpayments (Rust reference) - OR https://github.com/bitcoin/bitcoin/pull/28122 (Bitcoin Core) -2. Pick one of: - - Add Python test cases to rust-silentpayments (if they have test suite) - - Create comprehensive documentation for sender algorithm - - Implement sender in a new language (Go, Rust, JavaScript) - - Add test vector validation to an existing implementation -3. Submit PR with: - - Complete sender implementation - - Test cases (at least 10 test vectors) - - Documentation of algorithm -4. Link in Phase 2 submission - -**Alternative:** Create Educational Implementation -1. Create a `python-silentpayments` library on GitHub -2. Implement sender (and bonus: receiver protocol) -3. Include comprehensive documentation -4. Submit as a new repo (mention in your Phase 2 submission) - -### Daily Life Privacy Tip - -**Test your Silent Payments sender on Bitcoin testnet.** - -Before deploying production code: - -1. Generate a testnet SP address in Cake Wallet -2. Use your sender code to create a payment to that address -3. Broadcast on testnet -4. Watch Cake Wallet detect and confirm the payment -5. Verify you can spend the output - -This validates your implementation against real Bitcoin before going mainnet. - -### Reading List with Links - -1. **BIP352 Sender Algorithm (Section 2)** - https://github.com/bitcoin/bips/blob/master/bip-0352.md#sending - - Complete sender protocol specification - -2. **BIP352 Test Vectors** - https://github.com/bitcoin/bips/blob/master/bip-0352/vectors.json - - Reference test cases for validation - -3. **rust-silentpayments Reference Implementation** - https://github.com/cygnet3/rust-silentpayments - - Production-ready Rust implementation to compare - -4. **Elliptic Curve Cryptography Primer** - https://www.youtube.com/watch?v=F3zzNa42-tQ - - Visual explanation of ECDH (YouTube, 10 min) - -5. **BIP350: Bech32m Checksum Format** - https://github.com/bitcoin/bips/blob/master/bip-0350.md - - Address encoding/decoding specification - -6. **Taproot Key Aggregation and Tweaking** - https://github.com/bitcoin/bips/blob/master/bip-0340.md#public-key-tweaking - - Understanding point tweaking (used in SP) - -7. **Bitcoin Core Silent Payments PR Comments** - https://github.com/bitcoin/bitcoin/pull/28122 - - Implementer discussion and design decisions - ---- - -## Session 7: SP Scanning & Light Clients - -### Real-World Scenario - -**The Scanning Nightmare** -You build a Bitcoin mobile wallet with Silent Payments support. Your users publish SP addresses and receive payments. But when they open the app, they expect their balance to update immediately-not after scanning 800,000 transactions. A traditional wallet downloads headers and filters transactions. A Silent Payments wallet must test every transaction to see if it contains a payment for the user. On 4G, this takes minutes. Your app crashes. Users complain. The solution: **light clients with server-assisted filtering**. The server scans transactions and creates filters that tell the client "these blocks might contain your payment." The client downloads only the relevant blocks. Privacy is nearly perfect (the server doesn't learn which outputs belong to you), and the app is fast. - -### Learning Objectives - -By the end of Session 7, you will: -- Understand why Silent Payments receivers need to scan (unlike traditional addresses) -- Know how light clients optimize scanning using block filters or other techniques -- Understand the privacy/performance/complexity trade-offs -- Evaluate different light client architectures (server-side filtering, client-side filtering, full node) -- Know which tools exist today (Cake Wallet, Kyoto, Floresta) and their approaches -- Implement or contribute to a light client scanning infrastructure - -### Privacy Tools You'll Use - -- **Cake Wallet** - Production SP scanning (built-in) -- **Kyoto** (https://github.com/rustaceanrob/kyoto) - Rust light client with SP support -- **Floresta** (https://github.com/commerceblock/floresta) - Utreexo-based light client -- **Bitcoin Core with pruning** - Full node SP scanning (reference) -- **BDK (Bitcoin Dev Kit)** - Wallet framework with SP support (planned) -- **electrs** - Electrum server (for understanding server-side filtering) - -### Hands-On Exercise: "Build a Light Client Scanner" - -**Part 1: Understand the Scanning Problem** - -**Step 1:** Estimate the computational cost: -``` -Bitcoin blocks per day: ~144 -Transactions per block: ~2500 (average) -Daily transactions: ~360,000 - -For a Silent Payments receiver: -- Must test each transaction's outputs -- ECDH + hash per output: ~1ms CPU -- 360,000 tx * 1ms = 6 minutes per day -- Mobile device: 3-4x slower = 18-24 minutes - -Solution: Filter to relevant transactions first -``` - -**Step 2:** Understand filter-based scanning: -``` -Traditional: Test all transactions -Filter-based: Only test transactions that match filter - -Filters used: -1. Output script pattern (P2TR outputs, which SP uses) -2. Sender-provided filter (server tells client which blocks to check) -3. BIP157 Compact Block Filters (standardized filters) - -Trade-off: Server learns which blocks you're interested in - (but not which outputs you own, if implemented carefully) -``` - -**Step 3:** Read Cake Wallet's approach: -- Visit https://github.com/cake-tech/cake_wallet/tree/master/lib/bitcoin -- Look for Silent Payments scanning code -- Document: Does it scan locally? Use a server? Both? - -**Step 4:** Read Kyoto's approach: -- Visit https://github.com/rustaceanrob/kyoto -- Read README and architecture docs -- Kyoto uses BIP157 filters for optimization -- Document: How does Kyoto balance privacy and performance? - -**Part 2: Implement a Naive Scanner (Educational)** - -**Step 5:** Create a basic scanning client in Python: - -```python -import requests -import json -from typing import List - -class SilentPaymentsScanner: - """ - Educational SP Scanner - - WARNING: This is naive and slow. Real implementations like Kyoto - are much more sophisticated. This is to understand the concept. - """ - - def __init__(self, sp_private_key: bytes): - self.sp_private_key = sp_private_key - self.sp_outputs_found = [] - - def scan_block(self, block_data: dict) -> List[dict]: - """ - Scan a single block for SP outputs. - - For each transaction in the block: - - Extract outputs - - Test if output is intended for this SP receiver - - Returns list of found outputs - """ - found = [] - - for tx in block_data['tx']: - for vout_idx, output in enumerate(tx['vout']): - if self.is_sp_output_for_me(output): - found.append({ - 'txid': tx['txid'], - 'vout': vout_idx, - 'amount': output['value'], - 'scriptPubKey': output['scriptPubKey']['hex'] - }) - - return found - - def is_sp_output_for_me(self, output: dict) -> bool: - """ - Test if an output is a Silent Payment for this receiver. - - Algorithm: - 1. Extract the output's public key - 2. Try to decrypt/verify it matches our key - 3. Return True if it does - - (This is simplified; real implementation is more complex) - """ - # Extract public key from output script - script = output['scriptPubKey']['hex'] - - # Check if it's P2TR (Taproot) - if not script.startswith('5120'): - return False - - pubkey = bytes.fromhex(script[4:]) # Skip OP_1 + length - - # Test if pubkey matches one of our SP outputs - # (Real implementation uses ECDH to derive expected keys) - return self.test_pubkey(pubkey) - - def test_pubkey(self, pubkey: bytes) -> bool: - """ - Test if a public key was derived for this SP address. - - Requires: - - Our scanning private key - - The sender's inputs (for shared secret) - - ECDH computation - """ - # Implementation would use ECDH here - pass - - def scan_blockchain(self, height_start: int, height_end: int): - """ - Scan a range of blocks for SP outputs. - - Real implementation would: - - Use BIP157 filters to skip irrelevant blocks - - Parallelize ECDH computation - - Cache results - """ - for block_height in range(height_start, height_end + 1): - block_data = self.fetch_block(block_height) - outputs = self.scan_block(block_data) - self.sp_outputs_found.extend(outputs) - - def fetch_block(self, height: int) -> dict: - """ - Fetch block data from a block explorer or node. - """ - # Use mempool.space API or bitcoin-cli - url = f"https://mempool.space/api/block/{height}" - response = requests.get(url) - return response.json() -``` - -**Step 6:** Understand filter optimization: - -```python -class FilterOptimizedScanner(SilentPaymentsScanner): - """ - Optimized scanner using BIP157 compact block filters. - - Instead of scanning all transactions, use pre-computed filters - to skip irrelevant blocks. - """ - - def get_filter_for_block(self, block_hash: str) -> bytes: - """ - Get BIP157 compact block filter for a block. - - BIP157 filters are ~20KB per block and can be downloaded quickly. - They tell us: "This block might contain outputs to P2TR addresses" - """ - # Fetch from filter server - url = f"https://filter.server/cfilter/{block_hash}" - return requests.get(url).content - - def test_filter(self, cfilter: bytes, pubkey: bytes) -> bool: - """ - Test if a pubkey might be in a block's filter. - - Returns False = definitely not in block - Returns True = maybe in block (need to download) - """ - # BIP157 Golomb-Rice coded set test - # Check if pubkey hash is in the filter - pass - - def scan_blockchain_with_filters(self, height_start: int, - height_end: int): - """ - Optimized scan: only download and test blocks that - might contain our outputs. - """ - for height in range(height_start, height_end + 1): - block_hash = self.get_block_hash(height) - cfilter = self.get_filter_for_block(block_hash) - - # If filter matches, we need to fetch full block data - if self.test_filter(cfilter, self.get_search_key()): - block_data = self.fetch_block(height) - self.scan_block(block_data) -``` - -**Step 7:** Compare scanning approaches: - -| Approach | Privacy | Speed | Complexity | Tools | -|---|---|---|---|---| -| **Full node scanning** | Perfect | Slow | High | Bitcoin Core + SP patch | -| **BIP157 filters** | Very good | Fast | Medium | Kyoto, Electrum (with SP) | -| **Server-assisted** | Good | Very fast | Low | Cake Wallet, Floresta | -| **Wallet API** | Limited | Instant | Very low | Centralized wallet | - -**Step 8:** Document your findings: - -``` -SCANNING APPROACH ANALYSIS: - -For my use case (mobile + privacy): -- Best: Kyoto with BIP157 filters - (Privacy: 9/10, Speed: 8/10, Complexity: 6/10) - -Why not full node scanning? -- Mobile doesn't have CPU/storage for full node -- Scanning would take hours - -Why not wallet API? -- Server learns exactly which outputs you own -- Privacy: 2/10 - -Recommendation: -- Use Kyoto or similar for production -- Test full node scanning locally for development -``` - -### Open a PR This Week - -**Contribute to Light Client Scanning** - -1. Visit https://github.com/rustaceanrob/kyoto - OR https://github.com/commerceblock/floresta -2. Pick one of: - - Add documentation for SP scanning architecture - - Implement BIP157 filter testing (if not present) - - Add test cases for scanning edge cases (no payments, multiple payments per block, etc.) - - Optimize filter matching for performance -3. Research existing issues tagged "scanning" or "performance" -4. Submit PR with code + documentation - -**Alternative:** Contribute to Bitcoin Core -1. Go to https://github.com/bitcoin/bitcoin/pull/28122 (Silent Payments PR) -2. Add documentation or test cases for scanning performance -3. Propose optimizations for light client support -4. Submit PR - -### Daily Life Privacy Tip - -**Use Cake Wallet to see Silent Payments scanning in action.** - -Cake Wallet handles all scanning internally: - -1. Open Cake Wallet -2. Create a Bitcoin wallet -3. Go to Receive and copy your SP address -4. Send a small amount of Bitcoin to it (on testnet) -5. Watch the wallet's status as it scans -6. Your payment will appear once scan is complete - -This shows you: Silent Payments are practical today, and scanning is handled transparently. Cake Wallet's app doesn't slow down, even with SP enabled. - -### Reading List with Links - -1. **BIP352 Scanning (Section 3)** - https://github.com/bitcoin/bips/blob/master/bip-0352.md#scanning - - Receiver scanning algorithm specification - -2. **BIP157: Compact Block Filters** - https://github.com/bitcoin/bips/blob/master/bip-0157.md - - Filter-based block scanning optimization - -3. **Kyoto Light Client** - https://github.com/rustaceanrob/kyoto - - Production-ready Rust implementation with SP support - -4. **Floresta: Utreexo-Based Light Client** - https://github.com/commerceblock/floresta - - Alternative approach to efficient scanning - -5. **"Silent Payments Scanning Optimization" - Bitcoin Optech** - https://bitcoinops.org/en/newsletters/2024/01/10/#silent-payments-scanning-optimization - - Latest research on efficient scanning - -6. **Cake Wallet Architecture** - https://cakewallet.com/articles/technical-details - - How production wallet implements SP scanning - -7. **BDK (Bitcoin Dev Kit) - Wallet Framework** - https://github.com/bitcoindevkit/bdk - - Wallet library with SP support (in development) - ---- - -## Session 8: Contributing to Silent Payments - -### Real-World Scenario - -**The Production Moment** -Your Silent Payments implementation is tested. You've submitted PRs. Now comes the hard part: getting your code merged into production. Bitcoin Core PR #28122 has 50+ comments. You need to address reviewer feedback on cryptographic correctness, test coverage, and performance. A wallet developer is waiting for your light client optimization to improve their app's speed. A freelancer's platform is ready to deploy Silent Payments but needs wallet integration. This session is about professional-grade contribution: understanding the Bitcoin development process, responding to code reviews, ensuring production readiness, and getting your privacy infrastructure into the hands of users. - -### Learning Objectives - -By the end of Session 8, you will: -- Understand the Bitcoin Core development and review process -- Know which Silent Payments repositories are most impactful -- Write production-ready code with comprehensive tests -- Respond professionally to code review feedback -- Know the path to getting features merged and deployed -- Build a portfolio of Privacy infrastructure contributions - -### Privacy Tools You'll Use - -- **Bitcoin Core** (https://github.com/bitcoin/bitcoin) - - PR #28122: Silent Payments (in active review) - - PR #30152: BIP352 integration (planned/in progress) -- **rust-silentpayments** (https://github.com/cygnet3/rust-silentpayments) - - Reference implementation, good for tests and examples -- **Sparrow Wallet** (https://github.com/sparrowwallet/sparrow) - - Upcoming Silent Payments support -- **Cake Wallet** (https://github.com/cake-tech/cake_wallet) - - Production Silent Payments wallet -- **Bitcoin Optech** (https://bitcoinops.org/) - - Tracking implementation status and PRs - -### Hands-On Exercise: "Professional Contribution Workflow" - -**Part 1: Understand the Contribution Landscape** - -**Step 1:** Map existing Silent Payments PRs: -``` -SILENT PAYMENTS CONTRIBUTION MAP: - -Bitcoin Core: - - PR #28122: Full BIP352 implementation (in review since 2023) - Status: Core implementation, active review - Needs: Testing, documentation, optimization - Impact: HIGHEST (enables all other implementations) - - - PR #30152: Additional improvements (TBD) - Status: Planned - Needs: TBD - -Rust Reference: - - rust-silentpayments (https://github.com/cygnet3/rust-silentpayments) - Status: Production ready - Needs: Wallet integration, testing - -Wallets: - - Cake Wallet: Already ships SP (complete) - - Sparrow Wallet: SP planned (https://github.com/sparrowwallet/sparrow) - Needs: Integration, testing - - Bitcoin Core: Integrated in PR #28122 - -Light Clients: - - Kyoto: SP support ready - Needs: Documentation, optimization - - Floresta: SP planned - Needs: Implementation, testing -``` - -**Step 2:** Read Bitcoin Core PR #28122: -- Go to https://github.com/bitcoin/bitcoin/pull/28122 -- Read the initial description -- Scroll through comments (50+) -- Note: What are reviewers asking for? - - Test coverage - - Documentation - - Performance - - Cryptographic correctness - -**Step 3:** Evaluate the top 3 contribution opportunities: -``` -OPPORTUNITY 1: Bitcoin Core Test Coverage -Impact: HIGH (PR #28122 is blocked on test completeness) -Difficulty: MEDIUM -Path: Write comprehensive test cases for sender/receiver protocol -Time: 20-40 hours -Expected result: PR merged, feature in Bitcoin Core - -OPPORTUNITY 2: Sparrow Wallet Integration -Impact: MEDIUM (wallet used by 50k+ users) -Difficulty: MEDIUM -Path: Implement SP receiver in Sparrow, test with rust-silentpayments -Time: 30-50 hours -Expected result: SP support in next Sparrow release - -OPPORTUNITY 3: Light Client Optimization -Impact: MEDIUM (Kyoto + Floresta need performance work) -Difficulty: HARD -Path: Optimize filter matching, parallelize ECDH -Time: 40-60 hours -Expected result: Mobile SP wallets that scan in <1 minute - -OPPORTUNITY 4: Documentation -Impact: MEDIUM (SP is hard to understand; docs help adoption) -Difficulty: LOW -Path: Write guides, test vectors, protocol explainers -Time: 10-20 hours -Expected result: Bitcoin Optech article, Sparrow docs, community appreciation -``` - -**Part 2: Detailed Walkthrough - Bitcoin Core Test Contribution** - -**Step 4:** Clone Bitcoin Core and examine test structure: -```bash -git clone https://github.com/bitcoin/bitcoin.git -cd bitcoin -git checkout origin/PR-28122 # or latest commit from PR - -# Find Silent Payments test files -find . -name "*silent*" -o -name "*sp*" - -# Likely files: -# src/test/silentpayments_tests.cpp -# src/wallet/test/sp_wallet_tests.cpp -``` - -**Step 5:** Understand the test structure: -```cpp -// Example Bitcoin Core test structure: - -#include -#include - -BOOST_FIXTURE_TEST_SUITE(silentpayments_tests, BasicTestingSetup) - -BOOST_AUTO_TEST_CASE(sp_address_encoding) -{ - // Test valid SP address encoding/decoding - std::string sp_addr = "sp1qq..."; - auto [scan_key, spend_key] = DecodeSilentPaymentsAddress(sp_addr); - - // Verify round-trip encoding - auto reencoded = EncodeSilentPaymentsAddress(scan_key, spend_key); - BOOST_CHECK_EQUAL(sp_addr, reencoded); -} - -BOOST_AUTO_TEST_CASE(sp_sender_protocol) -{ - // Test sender deriving output - // ... -} - -BOOST_AUTO_TEST_CASE(sp_receiver_scanning) -{ - // Test receiver finding payments - // ... -} - -BOOST_AUTO_TEST_SUITE_END() -``` - -**Step 6:** Write a new test case for edge cases: -```cpp -BOOST_AUTO_TEST_CASE(sp_multiple_payments_same_block) -{ - // Test: Receiver gets 3 payments in same block - // All should be correctly identified and spendable - - std::string sp_address = "sp1qq..."; - - // Create 3 transactions to same SP address - CMutableTransaction tx1, tx2, tx3; - // ... populate tx1, tx2, tx3 ... - - // Create block containing all 3 - CBlock block; - block.vtx.push_back(MakeTransactionRef(tx1)); - block.vtx.push_back(MakeTransactionRef(tx2)); - block.vtx.push_back(MakeTransactionRef(tx3)); - - // Scan block - auto outputs = ScanBlockForSilentPayments(block, sp_address); - - // Verify - BOOST_CHECK_EQUAL(outputs.size(), 3); - - // Verify all outputs are different - std::set output_keys; - for (auto& output : outputs) { - output_keys.insert(output.pubkey); - } - BOOST_CHECK_EQUAL(output_keys.size(), 3); // All unique -} -``` - -**Step 7:** Run the test suite: -```bash -cd bitcoin -./configure -make -make test TESTS_WANTJOUR=silentpayments_tests - -# Should show: -# PASS: silentpayments_tests -# Ran 47 test cases -``` - -**Step 8:** Create a GitHub issue for test coverage gaps: -1. Go to https://github.com/bitcoin/bitcoin/issues -2. Search for existing issues about SP testing -3. If not found, create one: -``` -Title: "Silent Payments: Test coverage for [specific scenario]" - -Body: -BIP352 requires testing for: -- [ ] Multiple payments in single block (done in Session 8) -- [ ] Multiple payments in multiple blocks -- [ ] Maximum output count (999) -- [ ] Address encoding/decoding round-trip -- [ ] Fee calculation with SP outputs -- [ ] Integration with coin selection - -Currently missing: [List what you'll add] -``` - -**Part 3: Code Review Response Workflow** - -**Step 9:** Get feedback on your PR: -When you submit a PR to Bitcoin Core or a wallet, reviewers will comment. Example: - -``` -Reviewer comment: -"Have you tested this against all BIP352 test vectors? -I don't see test cases for failure scenarios." -``` - -**Step 10:** Respond professionally: -``` -Response: -"Good point. I've added the following: - -1. Test cases for all vectors in bip-0352/vectors.json -2. Failure scenarios: - - Invalid SP address format → correct error message - - Mismatched sender/receiver keys → output not found - - Corrupted block data → graceful handling - -See commits [xxx], [yyy], [zzz] for changes." -``` - -**Step 11:** Update your PR with improvements: -```bash -git checkout your-feature-branch -# Make improvements based on feedback -git add src/test/silentpayments_tests.cpp -git commit -m "test: Add failure scenario tests for Silent Payments" -git push origin your-feature-branch -# GitHub automatically updates PR -``` - -### Open a PR This Week - -**Make Your Final Contribution** - -Choose ONE of these production-grade contributions: - -**Option A: Bitcoin Core PR #28122 Enhancement** -1. Read the PR and comments carefully -2. Identify a specific gap (test coverage, documentation, optimization) -3. Create a focused PR that addresses that gap -4. Example: "test: Add test cases for [specific scenario]" -5. Submit to bitcoin/bitcoin - -**Option B: Sparrow Wallet Integration** -1. Fork https://github.com/sparrowwallet/sparrow -2. Add Silent Payments receiver support -3. Integrate with rust-silentpayments library -4. Test against Cake Wallet SP address -5. Submit PR with comprehensive tests - -**Option C: Light Client Optimization** -1. Fork https://github.com/rustaceanrob/kyoto -2. Optimize scanning (parallelize ECDH, improve filters) -3. Benchmark before/after -4. Document improvements -5. Submit PR - -**Option D: Documentation/Educational** -1. Create a comprehensive SP guide -2. Real-world examples (podcast donations, freelancer invoicing, merchant payments) -3. Compare to other privacy solutions -4. Submit as Bitcoin Optech article, Sparrow docs, or GitHub wiki -5. Quality bar: 3000+ words, multiple code examples - -All options should: -- Have comprehensive tests -- Include documentation -- Be production-ready -- Be reviewed by community before merging - -### Daily Life Privacy Tip - -**Follow Bitcoin Core development and Silent Payments progress.** - -Subscribe to: -1. Bitcoin Optech Newsletter (weekly): https://bitcoinops.org/en/newsletters/ -2. Bitcoin Core PR #28122: https://github.com/bitcoin/bitcoin/pull/28122 (watch for updates) -3. Sparrow Wallet releases: https://www.sparrowwallet.com/ -4. Cake Wallet releases: https://cakewallet.com/ - -When a feature is released: -- Test it immediately -- Report bugs -- Give feedback -- Help improve the implementation - -Silent Payments deployment is happening NOW. Your involvement speeds up the process. - -### Reading List with Links - -1. **Bitcoin Core PR #28122: Silent Payments** - https://github.com/bitcoin/bitcoin/pull/28122 - - The main Silent Payments implementation effort - -2. **Bitcoin Core Contribution Guide** - https://github.com/bitcoin/bitcoin/blob/master/CONTRIBUTING.md - - How to submit PRs to Bitcoin Core - -3. **Effective Bitcoin Core PR Review** - https://bitcoinops.org/en/newsletters/2022/01/05/#effective-bitcoin-core-pr-review - - Understanding the review process - -4. **rust-silentpayments Examples** - https://github.com/cygnet3/rust-silentpayments/tree/master/examples - - Real-world usage examples - -5. **Sparrow Wallet Contribution Guide** - https://github.com/sparrowwallet/sparrow/blob/master/CONTRIBUTING.md - - How to contribute to Sparrow - -6. **Bitcoin Optech Silent Payments Status** - https://bitcoinops.org/en/topics/silent-payments/ - - Current implementation status across projects - -7. **Code Review Best Practices** - https://google.github.io/eng-practices/review/reviewer/ - - How to respond to and incorporate feedback - ---- - -## Phase 2 Capstone Project - -### Your Mission - -Submit a **production-grade Silent Payments contribution** that ships in a real Bitcoin wallet or tool: - -**Required:** -1. **Implement** sender, receiver, or light client component -2. **Test** with comprehensive test cases (minimum 50+ tests) -3. **Document** algorithm and usage clearly -4. **Submit as PR** to Bitcoin Core, Sparrow, Kyoto, or equivalent -5. **Engage professionally** with code reviewers -6. **Achieve merge** or get clear feedback path to merge - -**Deliverables:** -- [ ] Complete, tested implementation -- [ ] Pull request to production repository -- [ ] Code review engagement (respond to 5+ comments) -- [ ] Comprehensive test suite -- [ ] Documentation (algorithm, usage, examples) -- [ ] Evidence of merge or clear merge path - -### Success Metrics - -- Code follows Bitcoin Core/wallet coding standards -- All tests pass -- Implements BIP352 correctly (against test vectors) -- Documentation is clear to external developers -- Community engagement shows maturity - ---- - -## Getting Help - -- **Bitcoin Optech**: https://bitcoinops.org/ -- **Bitcoin Core PR #28122**: https://github.com/bitcoin/bitcoin/pull/28122 -- **Cake Wallet**: https://github.com/cake-tech/cake_wallet -- **Sparrow Wallet**: https://github.com/sparrowwallet/sparrow -- **Kyoto**: https://github.com/rustaceanrob/kyoto -- **Bitcoin Core Slack**: https://bitcoincoredev.slack.com/ (ask to join) - ---- - -## Beyond Phase 2 - -After completing Phase 2, you're ready for advanced privacy infrastructure: -- Layer 2 privacy (Lightning Network on private channels) -- Mixing protocols (CoinJoin variants) -- Cross-chain atomic swaps with privacy -- ZK-proof-based systems (future Bitcoin research) - -The knowledge you've built-chain analysis, transaction structure, UTXO management, cryptography, and production development-applies to all of these. - -You're now a **Bitcoin Privacy Infrastructure Developer**. Use this power responsibly. diff --git a/privacy-track/phase-2-silent-payments/session-06-implement-sender/silent_payments_sender.py b/privacy-track/phase-2-silent-payments/session-06-implement-sender/silent_payments_sender.py deleted file mode 100644 index d42c806..0000000 --- a/privacy-track/phase-2-silent-payments/session-06-implement-sender/silent_payments_sender.py +++ /dev/null @@ -1,342 +0,0 @@ -""" -Bitcoin Privacy Track — Module 2, Session 2: Silent Payments Sender -Code Orange Dev School | codeorange.dev - -Implement a minimal Silent Payments (BIP352) sender in Python. - -Prerequisites: -- Understanding of elliptic curve cryptography (Bitcoin Dojo Week 1-2) -- Python 3.8+ -- pip install secp256k1 (or use the pure-Python fallback below) - -References: -- BIP352: https://bips.dev/352/ -- Bitcoin Optech: https://bitcoinops.org/en/topics/silent-payments/ - -Instructions: -- Complete each function by filling in the code where indicated -- Run this file with: python3 silent_payments_sender.py -- Discuss your solutions at the weekly call -""" - -import hashlib -import secrets - - -# ============================================================ -# Helpers (provided) -# ============================================================ - -def sha256(data: bytes) -> bytes: - return hashlib.sha256(data).digest() - - -def tagged_hash(tag: str, data: bytes) -> bytes: - """BIP340-style tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data)""" - tag_hash = sha256(tag.encode()) - return sha256(tag_hash + tag_hash + data) - - -# Secp256k1 curve parameters -P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F -N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 -Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 - - -def point_add(p1, p2): - """Add two points on secp256k1. None represents the point at infinity.""" - if p1 is None: - return p2 - if p2 is None: - return p1 - - x1, y1 = p1 - x2, y2 = p2 - - if x1 == x2 and y1 != y2: - return None - - if x1 == x2: - s = (3 * x1 * x1 * pow(2 * y1, P - 2, P)) % P - else: - s = ((y2 - y1) * pow(x2 - x1, P - 2, P)) % P - - x3 = (s * s - x1 - x2) % P - y3 = (s * (x1 - x3) - y1) % P - return (x3, y3) - - -def scalar_mult(k, point): - """Multiply a point by a scalar on secp256k1.""" - result = None - addend = point - while k: - if k & 1: - result = point_add(result, addend) - addend = point_add(addend, addend) - k >>= 1 - return result - - -G = (Gx, Gy) - - -def privkey_to_pubkey(privkey: int) -> tuple: - """Derive the public key (point) from a private key (scalar).""" - return scalar_mult(privkey, G) - - -def serialize_pubkey(point: tuple, compressed: bool = True) -> bytes: - """Serialize a public key point to bytes.""" - x, y = point - if compressed: - prefix = b'\x02' if y % 2 == 0 else b'\x03' - return prefix + x.to_bytes(32, 'big') - return b'\x04' + x.to_bytes(32, 'big') + y.to_bytes(32, 'big') - - -# ============================================================ -# Exercise 1: Generate a Silent Payment Address -# ============================================================ -# -# A Silent Payment address contains two public keys: -# B_scan — used to detect payments (can be on a hot device) -# B_spend — used to spend received coins (kept cold) -# -# Both are derived from a master key at BIP352 derivation paths. -# For this exercise, we generate them directly from random scalars. - -def generate_silent_payment_keypair(): - """Generate a Silent Payment scan/spend keypair. - - Returns: - tuple: (b_scan_privkey, b_scan_pubkey, b_spend_privkey, b_spend_pubkey) - where privkeys are ints and pubkeys are (x, y) tuples - """ - # YOUR CODE HERE - # 1. Generate a random private key for scanning (b_scan) - # b_scan = random integer in range [1, N-1] - # 2. Derive B_scan = b_scan * G - # 3. Generate a random private key for spending (b_spend) - # 4. Derive B_spend = b_spend * G - # 5. Return (b_scan, B_scan, b_spend, B_spend) - - pass - - -# ============================================================ -# Exercise 2: Derive the Shared Secret -# ============================================================ -# -# When sending a Silent Payment, the sender computes a shared -# secret using ECDH between their input keys and the receiver's -# scan public key. -# -# The process: -# 1. Sum all input private keys: a_sum = a_1 + a_2 + ... (mod N) -# 2. Compute input_hash = hash(smallest_outpoint || A_sum) -# 3. Shared secret = input_hash * a_sum * B_scan - -def derive_shared_secret(input_privkeys: list, outpoints: list, B_scan: tuple) -> tuple: - """Derive the ECDH shared secret for a Silent Payment. - - Args: - input_privkeys: list of private keys (ints) for the sender's inputs - outpoints: list of outpoint bytes (txid + vout) for ordering - B_scan: receiver's scan public key (x, y) - - Returns: - tuple: the shared secret point (x, y) - """ - # YOUR CODE HERE - # 1. Sum all input private keys modulo N - # a_sum = sum(input_privkeys) % N - # - # 2. Compute A_sum = a_sum * G (the summed public key) - # - # 3. Sort outpoints lexicographically, take the smallest - # smallest_outpoint = sorted(outpoints)[0] - # - # 4. Compute input_hash = tagged_hash("BIP0352/Inputs", - # smallest_outpoint + serialize_pubkey(A_sum)) - # Convert to integer: input_hash_int = int.from_bytes(input_hash, 'big') % N - # - # 5. Compute the shared secret point: - # shared_secret = scalar_mult((input_hash_int * a_sum) % N, B_scan) - # - # 6. Return shared_secret - - pass - - -# ============================================================ -# Exercise 3: Compute the Output Key -# ============================================================ -# -# The output key is what appears on-chain as a Taproot output. -# It's derived from the shared secret and the receiver's spend key. -# -# output_key = B_spend + t_k * G -# where t_k = tagged_hash("BIP0352/SharedSecret", ser(shared_secret) || k) - -def compute_output_key(shared_secret: tuple, B_spend: tuple, k: int = 0) -> tuple: - """Compute the Silent Payment output key. - - Args: - shared_secret: the ECDH shared secret point (x, y) - B_spend: receiver's spend public key (x, y) - k: output index (0 for first output, 1 for second, etc.) - - Returns: - tuple: the output public key (x, y) for the Taproot output - """ - # YOUR CODE HERE - # 1. Serialize the shared secret's x-coordinate (32 bytes, big-endian) - # ss_bytes = shared_secret[0].to_bytes(32, 'big') - # - # 2. Compute the tweak: - # t_k = tagged_hash("BIP0352/SharedSecret", ss_bytes + k.to_bytes(4, 'big')) - # t_k_int = int.from_bytes(t_k, 'big') % N - # - # 3. Compute the tweak point: T = t_k_int * G - # - # 4. Add to the spend key: output_key = point_add(B_spend, T) - # - # 5. Return output_key - - pass - - -# ============================================================ -# Exercise 4: Full Silent Payment Send -# ============================================================ -# -# Combine exercises 1-3 to simulate a full Silent Payment. - -def send_silent_payment(): - """Simulate sending a Silent Payment. - - 1. Receiver generates a Silent Payment address (scan + spend keys) - 2. Sender has some UTXOs with known private keys - 3. Sender computes the output key - 4. The on-chain output is a P2TR output paying to the output key - 5. Receiver can detect this payment using their scan key - """ - print("=" * 60) - print("Silent Payment Sender — Full Simulation") - print("=" * 60) - - # YOUR CODE HERE - # - # Step 1: Receiver generates their Silent Payment keypair - # result = generate_silent_payment_keypair() - # b_scan, B_scan, b_spend, B_spend = result - # print(f"\nReceiver's scan pubkey: {serialize_pubkey(B_scan).hex()}") - # print(f"Receiver's spend pubkey: {serialize_pubkey(B_spend).hex()}") - # - # Step 2: Sender has 2 inputs (simulated) - # sender_key_1 = secrets.randbelow(N - 1) + 1 - # sender_key_2 = secrets.randbelow(N - 1) + 1 - # outpoint_1 = secrets.token_bytes(36) # simulated txid:vout - # outpoint_2 = secrets.token_bytes(36) - # - # Step 3: Sender derives the shared secret - # shared_secret = derive_shared_secret( - # [sender_key_1, sender_key_2], - # [outpoint_1, outpoint_2], - # B_scan - # ) - # print(f"\nShared secret x: {shared_secret[0]}") - # - # Step 4: Sender computes the output key - # output_key = compute_output_key(shared_secret, B_spend, k=0) - # print(f"\nOutput key (on-chain Taproot address):") - # print(f" x: {output_key[0]}") - # print(f" Serialized: {serialize_pubkey(output_key).hex()}") - # - # Step 5: Verify — receiver can derive the same output key - # (Receiver uses b_scan instead of a_sum to get the same shared secret) - # a_sum = (sender_key_1 + sender_key_2) % N - # A_sum = privkey_to_pubkey(a_sum) - # smallest_outpoint = sorted([outpoint_1, outpoint_2])[0] - # input_hash = tagged_hash("BIP0352/Inputs", - # smallest_outpoint + serialize_pubkey(A_sum)) - # input_hash_int = int.from_bytes(input_hash, 'big') % N - # - # # Receiver's ECDH: b_scan * (input_hash * A_sum) - # tweaked_A = scalar_mult(input_hash_int, A_sum) - # receiver_shared_secret = scalar_mult(b_scan, tweaked_A) - # - # assert receiver_shared_secret == shared_secret, "Shared secrets don't match!" - # print("\n✓ Receiver derived the same shared secret") - # - # receiver_output_key = compute_output_key(receiver_shared_secret, B_spend, k=0) - # assert receiver_output_key == output_key, "Output keys don't match!" - # print("✓ Receiver derived the same output key — payment detected!") - # - # print(f"\n{'=' * 60}") - # print("On-chain, this looks like a normal Taproot output.") - # print("Only the receiver (with b_scan) can detect it's theirs.") - # print(f"{'=' * 60}") - - pass - - -# ============================================================ -# TESTS -# ============================================================ - -def run_tests(): - print("=" * 60) - print("Bitcoin Privacy Track — Silent Payments Sender Tests") - print("=" * 60) - - # Test 1: Keypair generation - print("\nTest 1: Keypair generation") - result = generate_silent_payment_keypair() - if result is None: - print(" SKIPPED — generate_silent_payment_keypair() not implemented yet") - else: - b_scan, B_scan, b_spend, B_spend = result - assert B_scan == privkey_to_pubkey(b_scan), "B_scan doesn't match b_scan" - assert B_spend == privkey_to_pubkey(b_spend), "B_spend doesn't match b_spend" - print(" PASSED") - - # Test 2: Shared secret derivation - print("\nTest 2: Shared secret derivation") - if result is None: - print(" SKIPPED — depends on Exercise 1") - else: - test_privkeys = [secrets.randbelow(N - 1) + 1, secrets.randbelow(N - 1) + 1] - test_outpoints = [secrets.token_bytes(36), secrets.token_bytes(36)] - ss = derive_shared_secret(test_privkeys, test_outpoints, B_scan) - if ss is None: - print(" SKIPPED — derive_shared_secret() not implemented yet") - else: - assert ss[0] > 0 and ss[1] > 0, "Shared secret should be a valid point" - print(" PASSED") - - # Test 3: Output key computation - print("\nTest 3: Output key computation") - if result is None or ss is None: - print(" SKIPPED — depends on Exercises 1-2") - else: - ok = compute_output_key(ss, B_spend, k=0) - if ok is None: - print(" SKIPPED — compute_output_key() not implemented yet") - else: - assert ok[0] > 0 and ok[1] > 0, "Output key should be a valid point" - print(" PASSED") - - # Test 4: Full simulation - print("\nTest 4: Full Silent Payment simulation") - send_silent_payment() - - print("\n" + "=" * 60) - print("Done! Bring your solutions to the weekly call.") - print("=" * 60) - - -if __name__ == "__main__": - run_tests() diff --git a/privacy-track/phase-2-silent-payments/session-07-scanning-and-cbf/silent_payments_scanner.py b/privacy-track/phase-2-silent-payments/session-07-scanning-and-cbf/silent_payments_scanner.py deleted file mode 100644 index b8c7b50..0000000 --- a/privacy-track/phase-2-silent-payments/session-07-scanning-and-cbf/silent_payments_scanner.py +++ /dev/null @@ -1,236 +0,0 @@ -""" -Bitcoin Privacy Track — Module 2, Session 3: Silent Payments Scanner -Code Orange Dev School | codeorange.dev - -Implement a minimal Silent Payments receiver/scanner. - -The challenge: to detect a Silent Payment, the receiver must scan -every transaction in every block and perform an ECDH computation -to check if any output belongs to them. - -Prerequisites: -- Completed silent_payments_sender.py (Session 2) -- Understanding of ECDH and BIP352 key derivation - -Instructions: -- Complete each function by filling in the code where indicated -- Run this file with: python3 silent_payments_scanner.py -- Discuss your solutions at the weekly call -""" - -import hashlib -import secrets -import time - -# Import helpers from the sender exercise -# (In practice, you'd have these in a shared module) -from silent_payments_sender import ( - sha256, tagged_hash, P, N, G, Gx, Gy, - point_add, scalar_mult, privkey_to_pubkey, - serialize_pubkey, generate_silent_payment_keypair, - derive_shared_secret, compute_output_key -) - - -# ============================================================ -# Exercise 1: Scan a Single Transaction -# ============================================================ -# -# Given a transaction's input public keys and outputs, -# check if any output is a Silent Payment to the receiver. - -def scan_transaction(tx_input_pubkeys: list, tx_outpoints: list, - tx_output_keys: list, b_scan: int, B_spend: tuple) -> list: - """Scan a transaction for Silent Payments to the receiver. - - Args: - tx_input_pubkeys: list of public key points [(x,y), ...] from tx inputs - tx_outpoints: list of outpoint bytes from tx inputs - tx_output_keys: list of output public key points [(x,y), ...] (Taproot outputs) - b_scan: receiver's scan private key (int) - B_spend: receiver's spend public key (x, y) - - Returns: - list of tuples: [(output_index, output_key), ...] for detected payments - """ - # YOUR CODE HERE - # - # 1. Sum all input public keys: A_sum = P1 + P2 + ... - # (Use point_add repeatedly) - # - # 2. Find the smallest outpoint (lexicographic sort) - # - # 3. Compute input_hash = tagged_hash("BIP0352/Inputs", - # smallest_outpoint + serialize_pubkey(A_sum)) - # input_hash_int = int.from_bytes(input_hash, 'big') % N - # - # 4. Compute the tweaked sum: tweaked_A = input_hash_int * A_sum - # - # 5. Receiver's ECDH: shared_secret = b_scan * tweaked_A - # - # 6. For each output index k, compute: - # expected_output = compute_output_key(shared_secret, B_spend, k) - # If expected_output matches tx_output_keys[k], it's a payment to us! - # - # 7. Return list of (output_index, output_key) for matches - - pass - - -# ============================================================ -# Exercise 2: Scan a Block -# ============================================================ -# -# Scan all transactions in a simulated block. - -def scan_block(block_txs: list, b_scan: int, B_spend: tuple) -> list: - """Scan all transactions in a block for Silent Payments. - - Args: - block_txs: list of dicts, each with keys: - 'input_pubkeys': list of (x,y) tuples - 'outpoints': list of bytes - 'output_keys': list of (x,y) tuples - b_scan: receiver's scan private key - B_spend: receiver's spend public key - - Returns: - list of tuples: [(tx_index, output_index, output_key), ...] - """ - # YOUR CODE HERE - # - # For each transaction in block_txs: - # results = scan_transaction(tx['input_pubkeys'], tx['outpoints'], - # tx['output_keys'], b_scan, B_spend) - # For each match, append (tx_index, output_index, output_key) - # - # Return all matches - - pass - - -# ============================================================ -# Exercise 3: Benchmark Scanning Performance -# ============================================================ -# -# How fast can you scan? This exercise generates simulated blocks -# and measures scanning time. - -def benchmark_scanning(num_blocks: int = 10, txs_per_block: int = 50): - """Benchmark Silent Payment scanning performance. - - Generates simulated blocks with random transactions, plants - one Silent Payment in a random block, and measures how long - it takes to find it. - """ - print(f"\nBenchmark: scanning {num_blocks} blocks x {txs_per_block} txs") - - # Generate receiver keys - b_scan, B_scan, b_spend, B_spend = generate_silent_payment_keypair() - - # Generate simulated blocks - blocks = [] - planted_block = secrets.randbelow(num_blocks) - planted_tx = secrets.randbelow(txs_per_block) - - for block_idx in range(num_blocks): - txs = [] - for tx_idx in range(txs_per_block): - # Random sender keys - num_inputs = secrets.randbelow(3) + 1 - sender_keys = [secrets.randbelow(N - 1) + 1 for _ in range(num_inputs)] - input_pubkeys = [privkey_to_pubkey(k) for k in sender_keys] - outpoints = [secrets.token_bytes(36) for _ in range(num_inputs)] - - # Random outputs (2-4 per tx) - num_outputs = secrets.randbelow(3) + 2 - output_keys = [privkey_to_pubkey(secrets.randbelow(N - 1) + 1) - for _ in range(num_outputs)] - - # Plant a real Silent Payment in one specific transaction - if block_idx == planted_block and tx_idx == planted_tx: - shared_secret = derive_shared_secret(sender_keys, outpoints, B_scan) - real_output = compute_output_key(shared_secret, B_spend, k=0) - output_keys[0] = real_output - print(f" Planted SP in block {block_idx}, tx {tx_idx}, output 0") - - txs.append({ - 'input_pubkeys': input_pubkeys, - 'outpoints': outpoints, - 'output_keys': output_keys - }) - blocks.append(txs) - - # YOUR CODE HERE - # Time the scanning process - # - # start = time.time() - # found = [] - # for block_idx, block_txs in enumerate(blocks): - # matches = scan_block(block_txs, b_scan, B_spend) - # for tx_idx, out_idx, out_key in matches: - # found.append((block_idx, tx_idx, out_idx)) - # elapsed = time.time() - start - # - # print(f"\n Scanned {num_blocks * txs_per_block} transactions in {elapsed:.3f}s") - # print(f" Rate: {num_blocks * txs_per_block / elapsed:.0f} tx/s") - # print(f" Found {len(found)} Silent Payment(s): {found}") - # - # # Mainnet has ~300,000 tx/day. How long would that take? - # mainnet_estimate = 300_000 / (num_blocks * txs_per_block / elapsed) - # print(f"\n Estimated mainnet scanning time: {mainnet_estimate:.0f}s ({mainnet_estimate/60:.1f} min) per day of blocks") - # - # QUESTION: Is this fast enough for a mobile wallet? - # QUESTION: How would compact block filters (BIP157/158) help? - - pass - - -# ============================================================ -# Run all exercises -# ============================================================ - -if __name__ == "__main__": - print("=" * 60) - print("Bitcoin Privacy Track — Silent Payments Scanner") - print("=" * 60) - - # Test scanning with a known payment - print("\nExercise 1-2: Scan for a planted Silent Payment") - - b_scan, B_scan, b_spend, B_spend = generate_silent_payment_keypair() - - if b_scan is None: - print(" Complete silent_payments_sender.py first!") - else: - # Create a transaction with a Silent Payment - sender_key = secrets.randbelow(N - 1) + 1 - outpoint = secrets.token_bytes(36) - - shared_secret = derive_shared_secret([sender_key], [outpoint], B_scan) - sp_output = compute_output_key(shared_secret, B_spend, k=0) - random_output = privkey_to_pubkey(secrets.randbelow(N - 1) + 1) - - if shared_secret is not None and sp_output is not None: - found = scan_transaction( - tx_input_pubkeys=[privkey_to_pubkey(sender_key)], - tx_outpoints=[outpoint], - tx_output_keys=[sp_output, random_output], - b_scan=b_scan, - B_spend=B_spend - ) - - if found is None: - print(" scan_transaction() not implemented yet") - elif len(found) == 1 and found[0][0] == 0: - print(f" PASSED — detected Silent Payment at output index {found[0][0]}") - else: - print(f" FAILED — expected 1 match at index 0, got: {found}") - - # Benchmark - print("\nExercise 3: Benchmark scanning performance") - benchmark_scanning(num_blocks=5, txs_per_block=20) - - print("\n" + "=" * 60) - print("Done! Bring your solutions and benchmark results to the weekly call.") - print("=" * 60) diff --git a/privacy-track/phase-3-payjoin/README.md b/privacy-track/phase-3-payjoin/README.md deleted file mode 100644 index c1fb2a2..0000000 --- a/privacy-track/phase-3-payjoin/README.md +++ /dev/null @@ -1,641 +0,0 @@ -# Phase 3: Payjoin (BIP77/78) - Sessions 9-12 - -**Objective:** Master Payjoin as a protocol that defeats Common Input Ownership Heuristic (CIOH) by design, understand the full stack from specification to production deployment, and contribute to the Payjoin ecosystem. - -**Why This Matters:** Payjoin is the most elegant privacy tool ever specified in Bitcoin. A single transaction can make a payment while looking like a self-transfer to all external observers. By the end of Phase 3, you'll understand how it works, build a complete sender/receiver implementation, deploy it in production, and contribute upstream. - ---- - -## Session 9: BIP77/78 Theory - How Payjoin Defeats Chain Analysis - -### Real-World Scenario - -**The Problem:** -A merchant runs a small coffee shop and uses BTCPay Server to accept Bitcoin payments. Alice walks in, pays 0.5 BTC, and the transaction lands on-chain with two inputs: -- Input A: BTCPay merchant's hot wallet -- Input B: Alice's wallet - -A chain analyst sees both inputs and applies CIOH: "Both inputs came from the same transaction, so they must belong to the same person." They conclude Alice now controls the merchant's address. - -In reality, Alice just bought a coffee. - -**The Solution:** -With Payjoin enabled on BTCPay, Alice's wallet proposes a payment. Before the merchant broadcasts it, the merchant adds their own input and changes the output destinations. The final transaction looks like: -- Input 1: Merchant's address -- Input 2: Alice's address (change gets routed back to Alice, not visible) -- Output 1: Merchant's new address -- Output 2: Looks like the merchant's change - -Now CIOH breaks. The analyst sees two inputs and can't tell who paid whom. The output structure is indistinguishable from a self-transfer. - -**The Privacy Win:** -Every Payjoin transaction creates ambiguity at scale. If 10% of transactions are Payjoins, the analyst can no longer trust CIOH at all. - -### Learning Objectives - -- Understand the Common Input Ownership Heuristic and why it fails in Payjoin -- Learn the BIP77 (Payjoin sender) and BIP78 (Payjoin receiver) specifications -- Explain how input and output substitution breaks heuristics -- Identify which wallets support Payjoin and when to use each -- Calculate the privacy gain of Payjoin vs. standard CoinJoin - -### Privacy Tools You'll Use - -- **BTCPay Server** (BIP78 receiver) - production-grade merchant payments -- **Bull Bitcoin Wallet** (BIP77 sender) - mobile Payjoin creation -- **Sparrow Wallet** (BIP77 sender) - desktop Payjoin with full control -- **Wasabi Wallet** (planned BIP78 support) - receiver support coming -- **Payjoin Dev Kit** - reference implementation -- **bitcoin-cli** - transaction inspection and debugging - -### Detailed Hands-On Exercise - -**Part 1: Understand the Protocol Flow** - -1. **Download and read BIP77 and BIP78** from github.com/bitcoin/bips: - - BIP77: Payjoin Sender - - BIP78: Payjoin Receiver - - Focus on sections 2-4 (Request Format, Response Handling, Examples) - -2. **Trace a real Payjoin transaction** on-chain: - - Go to mempool.space - - Search for a transaction with the Payjoin flag (marked with orange "P" icon) - - Examine its inputs and outputs - - Try to determine the payment direction using only the on-chain data - - Note: You can't. That's the point. - -3. **Study the protocol exchange:** - - Sender creates PSBT (Partially Signed Bitcoin Transaction) - - Sender sends to receiver's Payjoin endpoint - - Receiver adds their input - - Receiver modifies outputs (adds their own, adjusts change) - - Receiver signs their input and returns modified PSBT - - Sender verifies, signs their input, broadcasts - -4. **Document each step** in a markdown file with annotated diagrams showing sender/receiver interaction - -**Part 2: Set Up a Local Payjoin Test** - -1. **Start a local Bitcoin Core node** (regtest): - ```bash - bitcoind -regtest -server -rpcuser=admin -rpcpassword=password - ``` - -2. **Generate 101 blocks** to have spendable UTXOs: - ```bash - bitcoin-cli -regtest generatetoaddress 101 $(bitcoin-cli -regtest getnewaddress) - ``` - -3. **Create test wallets:** - ```bash - bitcoin-cli -regtest createwallet sender - bitcoin-cli -regtest createwallet receiver - ``` - -4. **Fund both wallets:** - ```bash - SENDER_ADDR=$(bitcoin-cli -regtest -rpcwallet=sender getnewaddress) - RECEIVER_ADDR=$(bitcoin-cli -regtest -rpcwallet=receiver getnewaddress) - - bitcoin-cli -regtest generatetoaddress 1 $SENDER_ADDR - bitcoin-cli -regtest generatetoaddress 1 $RECEIVER_ADDR - ``` - -5. **Inspect a PSBT:** - ```bash - bitcoin-cli -regtest decodepsbt "$PSBT_HEX" - ``` - - Identify inputs and outputs - - Note the fee amount - - Count required signatures - -**Part 3: Compare Privacy Models** - -Create a table showing: -- Standard payment: single input (sender), visible output (receiver) -- Coinjoin: many inputs, many outputs, extreme privacy, not practical for commerce -- Payjoin: two inputs (sender + receiver), outputs look like change, perfect for payments - -Measure: -- Privacy per transaction -- User experience friction -- Real-world deployment status -- Network impact - -### Open a PR This Week - -**Target: payjoin/rust-payjoin** -- Repository: https://github.com/payjoin/rust-payjoin -- Difficulty: Start with documentation - 1. Read the README and examples - 2. Run the tests: `cargo test` - 3. Choose one: Add a detailed receiver example, improve error docs, or add a Getting Started guide - 4. Open a PR with the improvement - -**OR Target: btcpayserver/btcpayserver** -- Look for Payjoin-related issues labeled "good-first-issue" -- Common contributions: Payjoin setup docs, test coverage, UI improvements - -### Daily Life Privacy Tip - -**Enable Payjoin on Your BTCPay Server Today** - -If you run a BTCPay Server: -1. Go to Store Settings → Payment Methods → Bitcoin -2. Find "Enable Payjoin" and toggle it on -3. From now on, every customer payment is a Payjoin - -Result: Your business becomes a privacy node in the Bitcoin network. Every payment you receive makes CIOH less reliable for everyone. - -### Reading List - -- **BIP77 (Payjoin Sender):** https://github.com/bitcoin/bips/blob/master/bip-0077.mediawiki -- **BIP78 (Payjoin Receiver):** https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki -- **Payjoin Spec (Human-Readable):** https://github.com/payjoin/payjoin/blob/master/README.md -- **Bull Bitcoin Wallet (BIP77):** https://www.bullbitcoin.com -- **Sparrow Wallet Payjoin Guide:** https://sparrowwallet.com -- **BTCPay Server Payjoin Docs:** https://docs.btcpayserver.org/Payjoin/ -- **Mempool Payjoin Finder:** https://mempool.space - ---- - -## Session 10: Building with Payjoin Dev Kit in Rust - -### Real-World Scenario - -**The Challenge:** -You're building a payment processor that should integrate with merchant wallets. You could integrate Payjoin one wallet at a time, but that's vendor-locked. Instead, you want to speak the standardized BIP77/78 protocol. - -**Your Solution:** -The Payjoin Development Kit (PDK) is a Rust library that handles all protocol complexity. You implement sender logic once, and it works with any BIP78-compliant receiver. - -This session, you'll build both sender and receiver implementations from scratch. - -### Learning Objectives - -- Understand the Payjoin Development Kit architecture -- Build a complete BIP78 receiver from scratch -- Build a complete BIP77 sender from scratch -- Debug PSBT construction and signing flows -- Test Payjoin transactions on regtest - -### Privacy Tools You'll Use - -- **Payjoin Development Kit (PDK)** - https://github.com/payjoin/rust-payjoin -- **Rust language** - stable edition 2021+ -- **Cargo** - dependency management -- **bitcoin-cli** - transaction inspection -- **Bitcoin Core (regtest)** - local testing - -### Detailed Hands-On Exercise - -**Part 1: Environment Setup** - -1. **Install Rust:** - ```bash - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - rustup default stable - ``` - -2. **Start Bitcoin Core on regtest:** - ```bash - bitcoind -regtest -server -rpcuser=admin -rpcpassword=password - ``` - -3. **Create a new Rust project:** - ```bash - cargo new payjoin_implementation - cd payjoin_implementation - ``` - -**Part 2: Configure Dependencies** - -Edit Cargo.toml with exact versions: -```toml -[package] -name = "payjoin_implementation" -version = "0.1.0" -edition = "2021" - -[dependencies] -payjoin = "0.13" -bitcoin = "0.31" -bitcoincore-rpc = "0.18" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -tokio = { version = "1", features = ["full"] } -axum = "0.7" -anyhow = "1.0" -``` - -**Part 3: Build Components** - -Implement `src/receiver.rs` for BIP78 receiver: -- Parse incoming PSBT from sender -- Add receiver's own UTXO as additional input -- Modify output destinations -- Sign receiver's input -- Return modified PSBT - -Implement `src/sender.rs` for BIP77 sender: -- Create unsigned PSBT with receiver address -- Fund with sender's UTXO -- POST to receiver's Payjoin endpoint -- Receive modified PSBT from receiver -- Sign sender's input and broadcast - -**Part 4: Integration Testing** - -Write tests that: -- Create a Payjoin PSBT on regtest -- Verify receiver adds its input -- Verify outputs are modified -- Ensure transaction is valid before broadcast - -### Open a PR This Week - -**Target: payjoin/rust-payjoin** - -1. **Review existing examples** at https://github.com/payjoin/rust-payjoin/tree/master/payjoin/examples - -2. **Contribute:** - - Write `examples/complete_payjoin.rs` showing full sender + receiver flow - - Include comments explaining each protocol step - - Ensure code compiles and runs - -3. **PR checklist:** - - Code compiles cleanly - - Includes error handling - - Follows existing code style - - Has clear comments - -### Daily Life Privacy Tip - -**Use Payjoin for Every Payment You Receive** - -If you're a merchant or service provider: -1. **BTCPay Server:** Enable Payjoin in store settings -2. **Sparrow Wallet:** Accept Payjoin requests when offered -3. **Bull Bitcoin:** Request Payjoin from merchants - -Every Payjoin strengthens privacy for all Bitcoin users by degrading heuristic analysis. - -### Reading List - -- **Payjoin Dev Kit:** https://github.com/payjoin/rust-payjoin -- **Rust Book (chapters 1-10):** https://doc.rust-lang.org/book/ -- **Bitcoincore RPC Docs:** https://github.com/rust-bitcoin/rust-bitcoincore-rpc -- **PSBT Spec (BIP174):** https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki -- **Cargo Book:** https://doc.rust-lang.org/cargo/ - ---- - -## Session 11: Payjoin in Production (BTCPay, Bull Bitcoin, Wallets) - -### Real-World Scenario - -**Your Business:** -You run a small coffee shop with Bitcoin payments via BTCPay Server. Every morning, customers pay with Bitcoin. - -**The Problem (Before Payjoin):** -Every payment hits the blockchain with: -- Input 1: Your hot wallet address -- Output 1: Change back to you - -Chain analysts see: "This person controls both the input and the output. All their payments are now linked." - -**The Solution (With Payjoin):** -You enable Payjoin in BTCPay settings. Now: -- Alice sends 0.5 BTC -- Your wallet automatically adds its own input -- Outputs look scrambled to the analyst -- Result: Alice's payment is indistinguishable from your business consolidating change - -After a month of Payjoin payments, analysts can't track your money in or out. - -### Learning Objectives - -- Deploy Payjoin in production using BTCPay Server -- Configure wallet integrations (Sparrow, Bull Bitcoin) -- Monitor Payjoin adoption and privacy metrics -- Debug production issues (timeout, rejection, fee negotiation) -- Understand operational implications of Payjoin - -### Privacy Tools You'll Use - -- **BTCPay Server** (BIP78 receiver) - production merchant platform -- **Sparrow Wallet** (BIP77 sender) - desktop wallet integration -- **Bull Bitcoin Wallet** (BIP77 sender) - mobile wallet -- **Bitcoin Core** - transaction validation -- **Mempool.space** - monitor transactions -- **docker** - containerized deployment - -### Detailed Hands-On Exercise - -**Part 1: Deploy BTCPay Server Locally** - -1. **Clone and set up:** - ```bash - git clone https://github.com/btcpayserver/btcpayserver.git - cd btcpayserver - git checkout v1.13.0 - ``` - -2. **Configure with Docker Compose:** - ```bash - cp .env.example .env - sed -i 's/NBITCOIN_NETWORK=.*/NBITCOIN_NETWORK=regtest/' .env - docker compose up -d - ``` - -3. **Access at http://127.0.0.1:3000:** - - Create admin account - - Set up test store - -**Part 2: Enable Payjoin** - -1. **In BTCPay Admin Settings:** - - Go to Services → Bitcoin - - Verify connection to Bitcoin Core - -2. **Enable Payjoin on your store:** - - Store Settings → Bitcoin → Payjoin (BIP78) - - Toggle on - - Configure: 30-minute expiration, 50bp max fee increase - -3. **Verify endpoint is live:** - ```bash - curl http://localhost:3000/.well-known/payjoin - ``` - -**Part 3: Create Invoice and Receive Payment** - -1. **Create test invoice:** - - Go to Invoices → Create Invoice - - Enter amount: 0.5 BTC - -2. **Send Payjoin payment from Sparrow:** - - Open Sparrow Wallet - - Send tab → Paste Payjoin address (with ?pj= parameter) - - Sign and broadcast - -3. **Inspect on-chain:** - - Transaction now has 2 inputs (yours + sender's) - - Outputs scrambled to analysts - - CIOH is broken - -**Part 4: Monitor and Test** - -1. **Create 5-10 test invoices** -2. **Send Payjoin payments each time** -3. **Track metrics:** - - Success rate - - Average time to completion - - Fee negotiation results - -4. **Document failure cases:** - - Insufficient fee - - Small amounts - - Timeout behavior - -### Open a PR This Week - -**Target: btcpayserver/btcpayserver** - -1. **Find Payjoin issues:** - - https://github.com/btcpayserver/btcpayserver/issues - - Filter by "Payjoin" label - -2. **Common contributions:** - - Test case for Payjoin timeout handling - - Documentation of Payjoin settings - - UI improvements for Payjoin setup - -3. **Example PR:** - - Add test: "test_payjoin_timeout_broadcasts_unsigned" - - Verify fallback behavior when receiver times out - - File PR with detailed test steps - -### Daily Life Privacy Tip - -**If You Accept Bitcoin, Enable Payjoin Today** - -Whether you're a merchant, freelancer, or donation recipient: - -1. **BTCPay Server:** - - Settings → Bitcoin → Payjoin (BIP78) - - Toggle ON - -2. **Wasabi Wallet:** - - Enable Payjoin receiver mode (when available) - -3. **The Ripple Effect:** - - Every Payjoin transaction degrades heuristics globally - - After 10% adoption, analysts can't rely on CIOH - - You're making privacy the default - -### Reading List - -- **BTCPay Server Docs:** https://docs.btcpayserver.org -- **Payjoin Setup Guide:** https://docs.btcpayserver.org/Payjoin/ -- **Sparrow Wallet Documentation:** https://sparrowwallet.com/docs -- **Bull Bitcoin Mobile Wallet:** https://www.bullbitcoin.com -- **BTCPay Server GitHub:** https://github.com/btcpayserver/btcpayserver -- **Docker Compose for Bitcoin:** https://docs.docker.com/compose -- **Bitcoin Core RPC Docs:** https://developer.bitcoin.org/reference/rpc/ - ---- - -## Session 12: Contributing to Payjoin - Ecosystem & Protocol - -### Real-World Scenario - -**The Opportunity:** -Payjoin is young but growing. Gaps exist: -- Wallet integration is inconsistent -- The specification needs clarification -- Production deployments need better tooling -- Privacy properties need academic validation - -As a Bitcoin privacy developer, you can shape the protocol's future. This session, you'll find real issues and fix them. - -### Learning Objectives - -- Navigate the Payjoin ecosystem repositories -- Identify and prioritize contribution opportunities -- Write production-quality tests for Payjoin logic -- Improve protocol documentation and error handling -- Collaborate with the Payjoin working group - -### Privacy Tools You'll Use - -- **payjoin/rust-payjoin** - reference implementation -- **btcpayserver/btcpayserver** - production receiver -- **sparrowwallet/sparrow** - production sender -- **git** - version control -- **GitHub Issues & PRs** - collaboration -- **cargo test** - testing framework - -### Detailed Hands-On Exercise - -**Part 1: Understand the Ecosystem** - -1. **Map the repositories:** - ```bash - git clone https://github.com/payjoin/rust-payjoin.git - git clone https://github.com/btcpayserver/btcpayserver.git - git clone https://github.com/sparrowwallet/sparrow.git - ``` - -2. **Understand relationships:** - - rust-payjoin: Specification + reference implementation - - btcpayserver: Production receiver - - Sparrow: Production sender - -3. **Review open issues:** - ```bash - curl -s https://api.github.com/repos/payjoin/rust-payjoin/issues?state=open | jq '.[] | {number: .number, title: .title}' - ``` - -**Part 2: Choose a Contribution** - -**Option A: rust-payjoin - Protocol Robustness** - -Improve error handling: -- Add context to PSBT validation errors -- Example: Instead of "Invalid PSBT", return "Invalid PSBT: input 0 has no previous_output" -- Write test showing improved error messages - -**Option B: btcpayserver - Production Integration** - -Add monitoring: -- Track Payjoin success rate -- Log attempt vs. success metrics -- Document failure modes - -**Option C: Improve Error Messages** - -User-friendly failures: -- Instead of: "Payjoin negotiation failed (500)" -- Show: "Merchant is processing your payment. Payment sent as standard transaction." - -**Part 3: Fork and Set Up** - -1. **Fork the repository:** - ```bash - git clone https://github.com/YOUR_USERNAME/rust-payjoin.git - cd rust-payjoin - ``` - -2. **Create feature branch:** - ```bash - git checkout -b improve/payjoin-error-context - ``` - -3. **Verify the build:** - ```bash - cargo test - ``` - -**Part 4: Make Your Contribution** - -Example: Improve Error Context - -1. **Locate validation code:** - ```bash - grep -r "Invalid PSBT" src/ - ``` - -2. **Edit src/receive/mod.rs:** - - Add input index to error messages - - Write test showing improved error context - - Run: `cargo test test_unsigned_receiver_input_error` - -3. **Commit with clear message:** - ```bash - git add src/receive/mod.rs tests/validation.rs - git commit -m "Improve error context for unsigned receiver inputs - - - Add input index to error messages - - Add test case for unsigned input validation - - Helps developers debug Payjoin flows" - ``` - -**Part 5: Open Your PR** - -1. **Push to fork:** - ```bash - git push origin improve/payjoin-error-context - ``` - -2. **Open PR on GitHub:** - - Clear description of problem and solution - - Link to related issues - - Show test results - -3. **Respond to review feedback:** - - Make requested changes - - Push updates - -### Open a PR This Week - -Your PR should: -- Fix a real issue or improve documentation -- Include tests -- Follow existing code style -- Have clear commit messages -- Be mergeable (no conflicts) - -### Daily Life Privacy Tip - -**Join the Payjoin Community** - -Payjoin is a movement to make privacy default for payments. - -1. **Follow the discussion:** - - GitHub Discussions: https://github.com/payjoin/rust-payjoin/discussions - - Bitcoin-dev mailing list: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/ - -2. **Test new features:** - - Try every new release of rust-payjoin, Sparrow, BTCPay - - Test with friends - - Report bugs and UX issues - -3. **Advocate for Payjoin:** - - Tell merchants about BTCPay Payjoin - - Recommend Sparrow to senders - - Explain why CIOH is dead - -Every time someone uses Payjoin, a heuristic dies. - -### Reading List - -- **GitHub Payjoin:** https://github.com/payjoin -- **Payjoin Spec:** https://github.com/payjoin/payjoin -- **rust-payjoin Documentation:** https://docs.rs/payjoin/latest/payjoin/ -- **Open Source Contribution Guide:** https://opensource.guide/how-to-contribute/ -- **Git Workflow for Contributors:** https://guides.github.com/introduction/flow/ -- **Rust Testing Guide:** https://doc.rust-lang.org/book/ch11-00-testing.html - ---- - -## Phase 3 Summary & Next Steps - -**What You've Achieved:** -1. Understood BIP77/78 specification and privacy properties -2. Built complete Payjoin sender + receiver in Rust -3. Deployed production Payjoin with BTCPay Server -4. Contributed to open source Payjoin projects - -**Privacy Impact:** -- Every Payjoin transaction breaks CIOH for that transaction -- Merchant Payjoins are 10x more effective than individual Coinjoins -- Active deployment in BTCPay, Sparrow, Bull Bitcoin - -**Your Next Challenge (Phase 4):** -Payjoin solves transaction-level privacy (input ownership). Next: -- **Network privacy:** Who broadcasts transactions? (Dandelion++, Tor, I2P) -- **Address discovery privacy:** Which addresses are yours? (Compact Block Filters) -- **Scriptpubkey privacy:** What script type do you use? (Taproot) - -In Phase 4, you'll learn network and protocol layers of Bitcoin privacy. diff --git a/privacy-track/phase-3-payjoin/session-10-payjoin-dev-kit/pdk_integration/Cargo.toml b/privacy-track/phase-3-payjoin/session-10-payjoin-dev-kit/pdk_integration/Cargo.toml deleted file mode 100644 index 7c082ff..0000000 --- a/privacy-track/phase-3-payjoin/session-10-payjoin-dev-kit/pdk_integration/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pdk-exercise" -version = "0.1.0" -edition = "2021" -description = "Code Orange Privacy Track — Payjoin Dev Kit integration exercise" -license = "CC0-1.0" - -[dependencies] -payjoin = { version = "0.21", features = ["send", "receive", "v2"] } -bitcoin = "0.32" -bitcoincore-rpc = "0.19" -tokio = { version = "1", features = ["full"] } -anyhow = "1" diff --git a/privacy-track/phase-3-payjoin/session-10-payjoin-dev-kit/pdk_integration/src/main.rs b/privacy-track/phase-3-payjoin/session-10-payjoin-dev-kit/pdk_integration/src/main.rs deleted file mode 100644 index d051dc4..0000000 --- a/privacy-track/phase-3-payjoin/session-10-payjoin-dev-kit/pdk_integration/src/main.rs +++ /dev/null @@ -1,322 +0,0 @@ -// Bitcoin Privacy Track — Module 3: Payjoin Dev Kit Integration -// Code Orange Dev School | codeorange.dev -// -// Build a working Payjoin sender and receiver using the Payjoin Dev Kit (PDK). -// -// This exercise connects to a local Bitcoin Core regtest node and performs -// a real Payjoin transaction, demonstrating the full BIP77/78 flow. -// -// Prerequisites: -// - Bitcoin Core running in regtest mode with two wallets ("sender", "receiver") -// - Rust toolchain (1.70+) -// - Completed the manual Payjoin exercise (payjoin_testnet.md) -// -// Setup: -// bitcoind -regtest -daemon -server -rpcuser=bitcoin -rpcpassword=bitcoin -// bitcoin-cli -regtest createwallet "sender" -// bitcoin-cli -regtest createwallet "receiver" -// bitcoin-cli -regtest -rpcwallet=sender generatetoaddress 101 $(bitcoin-cli -regtest -rpcwallet=sender getnewaddress) -// -// Run: -// cargo run -// -// This exercise has 5 parts. Complete each TODO section. - -use anyhow::{Context, Result}; -use bitcoin::Amount; -use bitcoincore_rpc::{Auth, Client, RpcApi}; - -/// Connect to a Bitcoin Core regtest wallet. -fn connect_wallet(wallet_name: &str) -> Result { - let url = format!("http://127.0.0.1:18443/wallet/{}", wallet_name); - let auth = Auth::UserPass("bitcoin".to_string(), "bitcoin".to_string()); - Client::new(&url, auth).context(format!("Failed to connect to wallet '{}'", wallet_name)) -} - -// ============================================================ -// Exercise 1: Verify Environment -// ============================================================ -// -// Confirm both wallets exist, have funds, and are ready for Payjoin. - -fn verify_environment() -> Result<()> { - println!("=== Exercise 1: Verify Environment ===\n"); - - let sender = connect_wallet("sender")?; - let receiver = connect_wallet("receiver")?; - - // TODO: Get and print the balance of both wallets - // Use: sender.get_balance(None, None)? - // - // If the sender has no funds, mine 101 blocks: - // let addr = sender.get_new_address(None, None)?; - // sender.generate_to_address(101, &addr.assume_checked())?; - // - // Fund the receiver with 5 BTC from the sender: - // let recv_addr = receiver.get_new_address(None, None)?; - // sender.send_to_address(&recv_addr.assume_checked(), Amount::from_btc(5.0)?, ...)?; - // // Mine a block to confirm - // let addr = sender.get_new_address(None, None)?; - // sender.generate_to_address(1, &addr.assume_checked())?; - - // YOUR CODE HERE - - println!(" Environment verified!\n"); - Ok(()) -} - -// ============================================================ -// Exercise 2: Create the Original PSBT (Sender Side) -// ============================================================ -// -// The sender creates a standard transaction (PSBT) that pays the receiver. -// This is the "original" transaction before Payjoin modification. - -fn create_original_psbt() -> Result { - println!("=== Exercise 2: Create Original PSBT ===\n"); - - let sender = connect_wallet("sender")?; - let receiver = connect_wallet("receiver")?; - - // TODO: - // 1. Get a fresh address from the receiver - // let payment_addr = receiver.get_new_address(None, None)?; - // - // 2. Create a funded PSBT from the sender paying 1 BTC to the receiver - // Use: sender.wallet_create_funded_psbt( - // &[], // inputs (empty = auto-select) - // &[outputs], // outputs - // None, // locktime - // Some(options), // options - // None, // bip32derivs - // )?; - // - // 3. Print the PSBT details: - // - Number of inputs - // - Number of outputs - // - Fee amount - // - Which output is the payment and which is change - // - // 4. Sign the PSBT with the sender's wallet - // let signed = sender.wallet_process_psbt(&psbt, ...)?; - // - // 5. Return the signed PSBT as a base64 string - - // YOUR CODE HERE - - todo!("Implement create_original_psbt") -} - -// ============================================================ -// Exercise 3: Receiver Modifies the PSBT (The Payjoin) -// ============================================================ -// -// The receiver takes the sender's PSBT and adds their own input. -// This is the core Payjoin operation. - -fn receiver_modify_psbt(original_psbt: &str) -> Result { - println!("=== Exercise 3: Receiver Modifies PSBT ===\n"); - - let receiver = connect_wallet("receiver")?; - - // TODO: - // 1. Decode the original PSBT to inspect it - // let decoded = receiver.decode_psbt(original_psbt)?; - // Print: "Original TX has N inputs and M outputs" - // - // 2. List the receiver's UTXOs - // let utxos = receiver.list_unspent(None, None, None, None, None)?; - // Pick one UTXO to contribute to the Payjoin - // Print: "Receiver contributing UTXO: {txid}:{vout} ({amount} BTC)" - // - // 3. This is the hard part — construct a new PSBT that includes: - // a) All inputs from the original PSBT - // b) The receiver's additional input - // c) The payment output (increased by the receiver's input value) - // d) The sender's change output (unchanged) - // - // In production, the Payjoin Dev Kit handles this automatically. - // For this exercise, you can either: - // - Use PDK's receive module (preferred) - // - Manually construct the modified PSBT using createpsbt RPC - // - // 4. Sign the receiver's input - // let receiver_signed = receiver.wallet_process_psbt(&modified_psbt, ...)?; - // - // 5. Print the modified PSBT details: - // - New number of inputs (should be original + 1) - // - Updated output amounts - // - "Chain analyst sees: N inputs, M outputs — applies CIOH — WRONG" - // - // 6. Return the receiver-signed modified PSBT - - // YOUR CODE HERE - - todo!("Implement receiver_modify_psbt") -} - -// ============================================================ -// Exercise 4: Sender Verifies and Signs (Security Checks) -// ============================================================ -// -// The sender MUST verify the receiver's modifications before signing. -// A malicious receiver could steal funds if the sender doesn't check. - -fn sender_verify_and_sign(original_psbt: &str, modified_psbt: &str) -> Result { - println!("=== Exercise 4: Sender Verifies and Signs ===\n"); - - let sender = connect_wallet("sender")?; - - // TODO: Implement ALL of these verification checks. - // These are critical for security — a real wallet must perform every one. - // - // VERIFICATION CHECKLIST: - // - // 1. SENDER'S INPUTS UNCHANGED: - // All inputs from the original PSBT must still be present - // in the modified PSBT with identical scriptPubKeys. - // "Check: sender's inputs are preserved" - // - // 2. SENDER'S PAYMENT OUTPUT UNCHANGED: - // The output paying the receiver must have the same or greater value. - // (Greater is fine — the receiver is adding value.) - // "Check: payment output value >= original" - // - // 3. SENDER'S CHANGE OUTPUT UNCHANGED: - // The sender's change output must have the same value. - // If the receiver reduced the sender's change, they're stealing. - // "Check: sender's change output preserved" - // - // 4. NO NEW OUTPUTS TO UNKNOWN ADDRESSES: - // The modified PSBT should not add outputs to addresses - // the sender doesn't recognize. (The receiver's output increasing - // is expected; a new third output is suspicious.) - // "Check: no unexpected new outputs" - // - // 5. FEE IS REASONABLE: - // The total fee should not increase unreasonably. - // The receiver may slightly increase the fee (they added an input), - // but a massive fee increase could be an attack. - // "Check: fee increase is reasonable" - // - // 6. TRANSACTION IS VALID: - // The modified PSBT must be a valid Bitcoin transaction. - // - // If all checks pass: - // Sign the sender's inputs: sender.wallet_process_psbt(&modified_psbt, ...)? - // Finalize: sender.finalize_psbt(&both_signed, None)? - // Return the final raw transaction hex - // - // If any check fails: - // Print which check failed and return an error - // The sender should broadcast the ORIGINAL transaction instead - - // YOUR CODE HERE - - todo!("Implement sender_verify_and_sign") -} - -// ============================================================ -// Exercise 5: Broadcast and Analyze -// ============================================================ -// -// Broadcast the Payjoin transaction, then analyze it from a -// chain analyst's perspective. - -fn broadcast_and_analyze(final_tx_hex: &str) -> Result<()> { - println!("=== Exercise 5: Broadcast and Analyze ===\n"); - - let sender = connect_wallet("sender")?; - - // TODO: - // 1. Broadcast the transaction - // let txid = sender.send_raw_transaction(final_tx_hex)?; - // println!(" Payjoin txid: {}", txid); - // - // 2. Mine a block - // let addr = sender.get_new_address(None, None)?; - // sender.generate_to_address(1, &addr.assume_checked())?; - // - // 3. Decode the confirmed transaction - // let tx = sender.get_raw_transaction_info(&txid, None)?; - // - // 4. Print the chain analyst's view: - // println!("\n === Chain Analyst's View ==="); - // println!(" Inputs: {}", tx.vin.len()); - // println!(" Outputs: {}", tx.vout.len()); - // println!(); - // println!(" Applying CIOH: 'All {} inputs belong to the same entity'", tx.vin.len()); - // println!(" This conclusion is: WRONG"); - // println!(); - // println!(" Applying change detection:"); - // // Analyze which output looks like change - // // The analyst will likely identify the wrong entity as the sender - // println!(); - // println!(" The Payjoin has created ambiguity:"); - // println!(" - Possible interpretation 1: ..."); - // println!(" - Possible interpretation 2: ..."); - // println!(" - Possible interpretation 3: ..."); - // println!(" The analyst cannot determine which is correct."); - // - // 5. Now create a NORMAL (non-Payjoin) transaction for comparison - // Send the same amount to the same receiver, but without Payjoin - // Broadcast and decode it - // Print the same analysis - // Show how the normal transaction reveals much more information - - // YOUR CODE HERE - - todo!("Implement broadcast_and_analyze") -} - -// ============================================================ -// Main -// ============================================================ - -fn main() -> Result<()> { - println!("================================================================"); - println!("Bitcoin Privacy Track — Payjoin Dev Kit Integration"); - println!("Code Orange Dev School | codeorange.dev"); - println!("================================================================\n"); - - // Exercise 1: Verify environment - verify_environment()?; - - // Exercise 2: Sender creates original PSBT - let original_psbt = create_original_psbt()?; - - // Exercise 3: Receiver modifies the PSBT - let modified_psbt = receiver_modify_psbt(&original_psbt)?; - - // Exercise 4: Sender verifies and signs - let final_tx = sender_verify_and_sign(&original_psbt, &modified_psbt)?; - - // Exercise 5: Broadcast and analyze - broadcast_and_analyze(&final_tx)?; - - println!("\n================================================================"); - println!("REFLECTION QUESTIONS:"); - println!("================================================================"); - println!(); - println!("1. In Exercise 4, you implemented 5 security checks."); - println!(" Which check is the most important? What happens if"); - println!(" a wallet skips it?"); - println!(); - println!("2. The Payjoin Dev Kit handles the PSBT modification"); - println!(" automatically. What advantage does manual implementation"); - println!(" (as in this exercise) give you as a developer?"); - println!(); - println!("3. How would you modify this code to use BIP77 (V2)"); - println!(" with a Payjoin Directory instead of direct communication?"); - println!(); - println!("4. If you were integrating Payjoin into a mobile wallet,"); - println!(" what UX would you show the user during the PSBT"); - println!(" exchange? Should the user even know a Payjoin is"); - println!(" happening?"); - println!(); - println!("================================================================"); - println!("Done! Bring your code and analysis to the weekly session."); - println!("================================================================"); - - Ok(()) -} diff --git a/privacy-track/phase-3-payjoin/session-11-production-integration/btcpay_payjoin_lab.md b/privacy-track/phase-3-payjoin/session-11-production-integration/btcpay_payjoin_lab.md deleted file mode 100644 index 23a6362..0000000 --- a/privacy-track/phase-3-payjoin/session-11-production-integration/btcpay_payjoin_lab.md +++ /dev/null @@ -1,184 +0,0 @@ -# Payjoin Testnet Exercise - -> Build and send a Payjoin transaction on regtest. - -## Setup - -### Prerequisites - -- Bitcoin Core installed and running in regtest mode -- Rust toolchain installed (`rustup`) -- Payjoin Dev Kit cloned: `git clone https://github.com/payjoin/rust-payjoin` - -### Start Your Regtest Environment - -```bash -# Start bitcoind in regtest -bitcoind -regtest -daemon - -# Create two wallets (sender and receiver) -bitcoin-cli -regtest createwallet "sender" -bitcoin-cli -regtest createwallet "receiver" - -# Mine blocks to the sender (need 101 for maturity) -SENDER_ADDR=$(bitcoin-cli -regtest -rpcwallet=sender getnewaddress) -bitcoin-cli -regtest generatetoaddress 101 $SENDER_ADDR - -# Check sender balance -bitcoin-cli -regtest -rpcwallet=sender getbalance -# Should show 50.00000000 - -# Fund the receiver too (they need a UTXO to contribute to the Payjoin) -RECEIVER_ADDR=$(bitcoin-cli -regtest -rpcwallet=receiver getnewaddress) -bitcoin-cli -regtest -rpcwallet=sender sendtoaddress $RECEIVER_ADDR 5.0 -bitcoin-cli -regtest generatetoaddress 1 $SENDER_ADDR -``` - ---- - -## Exercise 1: Manual Payjoin (Step by Step) - -### Step 1: Receiver Creates a Payment Request - -```bash -# Receiver generates a fresh address for the payment -PAYMENT_ADDR=$(bitcoin-cli -regtest -rpcwallet=receiver getnewaddress) -echo "Payment address: $PAYMENT_ADDR" -``` - -### Step 2: Sender Creates the Original PSBT - -```bash -# Sender creates a PSBT paying 1 BTC to the receiver -ORIG_PSBT=$(bitcoin-cli -regtest -rpcwallet=sender walletcreatefundedpsbt \ - '[]' "[{\"$PAYMENT_ADDR\": 1.0}]" 0 '{"includeWatching": true}' | jq -r '.psbt') - -echo "Original PSBT: $ORIG_PSBT" - -# Decode it to see the structure -bitcoin-cli -regtest decodepsbt $ORIG_PSBT -``` - -**YOUR TASK**: Examine the decoded PSBT. Answer: -- How many inputs does it have? -- How many outputs? -- Which output is the payment and which is change? -- If a chain analyst sees this, what can they deduce? - -### Step 3: Sender Signs the Original PSBT - -```bash -SIGNED_ORIG=$(bitcoin-cli -regtest -rpcwallet=sender walletprocesspsbt $ORIG_PSBT | jq -r '.psbt') -echo "Signed original PSBT: $SIGNED_ORIG" -``` - -### Step 4: Receiver Adds Their Input (The Payjoin Magic) - -This is where Payjoin happens. The receiver contributes one of their own UTXOs. - -```bash -# Receiver lists their UTXOs -bitcoin-cli -regtest -rpcwallet=receiver listunspent - -# Pick one of the receiver's UTXOs (note the txid, vout, and amount) -# YOUR CODE: Fill in the UTXO details from the listunspent output -RECEIVER_UTXO_TXID="" -RECEIVER_UTXO_VOUT=0 # fill in -RECEIVER_UTXO_AMOUNT=5.0 # fill in -``` - -**YOUR TASK**: Now manually construct the modified Payjoin PSBT. - -The receiver needs to: -1. Add their UTXO as an additional input -2. Increase their payment output by the value of their added input -3. The change output from the sender stays the same - -```bash -# Create the Payjoin PSBT with both sender's and receiver's inputs -# The receiver's output increases from 1.0 BTC to 1.0 + RECEIVER_UTXO_AMOUNT -# (minus the receiver's share of the fee) - -# YOUR CODE HERE: -# Use walletcreatefundedpsbt or createpsbt to build the modified transaction -# Hint: You'll need to include: -# - All inputs from the original PSBT -# - The receiver's additional UTXO as a new input -# - The payment output (now 1.0 + receiver's UTXO amount) -# - The sender's change output (unchanged) -``` - -### Step 5: Both Sign the Payjoin PSBT - -```bash -# Receiver signs their input -RECEIVER_SIGNED=$(bitcoin-cli -regtest -rpcwallet=receiver walletprocesspsbt $PAYJOIN_PSBT | jq -r '.psbt') - -# Sender signs their input(s) -BOTH_SIGNED=$(bitcoin-cli -regtest -rpcwallet=sender walletprocesspsbt $RECEIVER_SIGNED | jq -r '.psbt') - -# Finalize -FINAL_TX=$(bitcoin-cli -regtest finalizepsbt $BOTH_SIGNED | jq -r '.hex') -``` - -### Step 6: Broadcast and Analyze - -```bash -# Broadcast -TXID=$(bitcoin-cli -regtest sendrawtransaction $FINAL_TX) -echo "Payjoin txid: $TXID" - -# Mine a block -bitcoin-cli -regtest generatetoaddress 1 $SENDER_ADDR - -# Decode the final transaction -bitcoin-cli -regtest getrawtransaction $TXID true -``` - -**YOUR TASK**: Analyze the final transaction: -1. How many inputs does it have? -2. Can you tell which input belongs to the sender and which to the receiver? -3. Apply the common-input-ownership heuristic - what would a chain analyst conclude? -4. Is that conclusion correct? - ---- - -## Exercise 2: Compare Normal vs Payjoin - -Create two transactions side by side: - -1. A **normal transaction**: sender pays receiver 1 BTC (no Payjoin) -2. A **Payjoin transaction**: sender pays receiver 1 BTC (with receiver input) - -For each transaction, document: - -| Property | Normal TX | Payjoin TX | -|----------|-----------|------------| -| Number of inputs | | | -| Number of outputs | | | -| Can identify change output? | | | -| CIOH conclusion | | | -| CIOH accuracy | | | -| Identifiable as Payjoin? | | | - ---- - -## Exercise 3: Discussion Questions - -Write 2-3 paragraphs on each: - -1. **What coin selection strategy should the receiver use when adding their input?** Should they add their largest UTXO? Smallest? Random? What are the privacy trade-offs? - -2. **What checks must the sender perform on the receiver's modified PSBT?** List every verification step needed to prevent the receiver from stealing funds or degrading the sender's privacy. - -3. **If you were integrating Payjoin into a mobile wallet, what UX decisions would you make?** Should it be automatic? Opt-in? How do you handle failures? - ---- - -## Submission - -Post your transaction analysis and discussion answers in the Discord `#privacy-track` channel before the next session. - ---- - -*Code Orange Dev School | [codeorange.dev](https://codeorange.dev) | CC0 1.0 Universal* diff --git a/privacy-track/phase-4-network-privacy/README.md b/privacy-track/phase-4-network-privacy/README.md deleted file mode 100644 index 80d5518..0000000 --- a/privacy-track/phase-4-network-privacy/README.md +++ /dev/null @@ -1,1087 +0,0 @@ -# Phase 4: Network & Protocol Privacy - Sessions 13-16 - -**Objective:** Master the network and protocol layers of Bitcoin privacy, learning how to hide transaction broadcast origins, discover addresses privately, and use Taproot for script obfuscation. - -**Why This Matters:** Payjoin solves "who paid whom" (Phase 3). But attackers still ask: "Who broadcast this transaction?" and "What addresses does this person own?" Phase 4 attacks these questions with Dandelion++, Compact Block Filters, light clients, and Taproot. By the end, you'll understand that true privacy requires layers: transaction privacy, network privacy, and scriptpubkey privacy working together. - ---- - -## Session 13: P2P Network Privacy (Dandelion++, Tor, I2P) - -### Real-World Scenario - -**The Attack:** -You create a transaction and broadcast it to the Bitcoin network. Your ISP-level adversary listens: -- They capture all traffic from your IP address -- They see a Bitcoin transaction leave your computer to multiple peers -- They match the timing and content -- They now know: "The person at IP 192.168.1.100 created transaction 0xabc123" - -Even if they can't link the transaction to your identity initially, they've created a powerful surveillance vector. They can correlate your IP with your behavior on other platforms. - -**The Solution:** -You enable Dandelion++ (RFC 6979 for Bitcoin) or route through Tor/I2P: -- Dandelion++ has two phases: "stem" (hops through 3-5 random nodes) and "fluff" (normal broadcast) -- An attacker needs to observe multiple transactions to derive the source -- Tor/I2P hides your IP entirely behind onion routing - -Result: Broadcasting a transaction no longer reveals your IP address. - -### Learning Objectives - -- Understand how network-level surveillance works (IP address correlation) -- Learn Dandelion++ protocol (stem/fluff phases, anonymity analysis) -- Configure Bitcoin Core to broadcast over Tor -- Set up I2P integration with Bitcoin Core -- Measure network privacy properties (latency vs. anonymity tradeoffs) - -### Privacy Tools You'll Use - -- **Bitcoin Core** - full node with Tor/I2P support -- **Tor** - onion routing network -- **I2P** - decentralized anonymity network -- **Dandelion++** - transaction broadcast privacy (conceptual) -- **bitcoin-cli getpeerinfo** - peer analysis -- **tcpdump** - network traffic inspection - -### Detailed Hands-On Exercise - -**Part 1: Understand the Attack** - -1. **Set up network monitoring:** - ```bash - # Start Bitcoin Core on testnet (not production) - bitcoind -testnet -listen -upnp=0 - - # In another terminal, monitor peer connections - bitcoin-cli -testnet getpeerinfo | jq '.[] | {addr: .addr, inbound: .inbound_onion}' - ``` - -2. **Create a transaction and observe broadcast:** - ```bash - # Send a test transaction - bitcoin-cli -testnet sendtoaddress $(bitcoin-cli -testnet getnewaddress) 0.001 - - # Verify it propagates to peers - bitcoin-cli -testnet getrawmempool | jq '.[] | select(. == "YOUR_TXID")' - ``` - -3. **Document what an observer sees:** - - Your IP address connects to Bitcoin peers - - Transaction broadcast originates from your IP - - Timing correlates with your transaction creation - - Conclusion: Your IP likely owns this transaction - -**Part 2: Configure Tor in Bitcoin Core** - -1. **Install Tor** (macOS/Linux): - ```bash - # macOS - brew install tor - - # Linux (Ubuntu) - sudo apt-get install tor - ``` - -2. **Start Tor:** - ```bash - tor # Run in background - ``` - -3. **Configure Bitcoin Core for Tor** (bitcoin.conf): - ``` - # Use Tor for outgoing connections - proxy=127.0.0.1:9050 - - # Listen as a Tor hidden service - listen=1 - bind=127.0.0.1 - onlynet=onion - - # Optional: use specific onion address - externalip=YOUR_ONION_ADDRESS.onion - ``` - -4. **Restart Bitcoin Core:** - ```bash - bitcoind -testnet -conf=/path/to/bitcoin.conf - ``` - -5. **Verify Tor integration:** - ```bash - bitcoin-cli -testnet getnetworkinfo | grep onion - ``` - -**Part 3: Set Up I2P (Advanced)** - -1. **Install I2P:** - ```bash - # Download from https://geti2p.net - # Or: brew install i2p (macOS) - i2pd # Run i2p daemon - ``` - -2. **Configure Bitcoin for I2P** (bitcoin.conf): - ``` - i2psam=127.0.0.1:7656 - onlynet=i2p - ``` - -3. **Monitor I2P connections:** - ```bash - bitcoin-cli -testnet getnetworkinfo | jq '.networks[] | select(.name == "i2p")' - ``` - -**Part 4: Understand Dandelion++ (Conceptual)** - -While Bitcoin Core doesn't implement Dandelion++ yet, understand the protocol: - -1. **Dandelion++ Phases:** - - Stem phase: transaction hops through 3-5 random nodes before broadcast - - Fluff phase: normal gossiping to the network - - Goal: Attacker needs to observe multiple transactions to trace back to source - -2. **Create a document explaining:** - - Why stem phase provides anonymity - - How an attacker could break Dandelion++ (observe full stem chain) - - Why combining Dandelion++ + Tor is optimal - -3. **Pseudocode for Dandelion++ flow:** - ``` - When creating a transaction: - ├─ Choose random peer for stem phase (probability p) - ├─ If stem: send to peer (don't broadcast yet) - ├─ If fluff: normal broadcast to all peers - └─ After stem timeout (60-300s): - ├─ If you're still in stem: flip coin - │ ├─ Heads: continue stem to next random peer - │ └─ Tails: start fluff phase - └─ If you're not original sender: flip coin - ├─ Heads: relay to random peer (continue stem) - └─ Tails: start fluff phase - ``` - -**Part 5: Measure Privacy Properties** - -1. **Compare broadcast patterns:** - - Standard Bitcoin Core: Observe latency from node to all peers - - Tor: Random latency, harder to trace - - I2P: Even higher variance - - Dandelion++: Asymmetric-harder to measure - -2. **Create a test:** - - Send 5 transactions without Tor - - Send 5 transactions with Tor - - Measure: average peer count, broadcast latency, peer address diversity - - Document findings - -3. **Privacy analysis:** - ```markdown - # Network Privacy Test Results - - ## Standard (clearnet) - - Peers: 10 - - Broadcast latency: 0-50ms (low variance) - - Conclusion: Easy to fingerprint source IP - - ## Tor - - Peers: 5 (Tor peers only) - - Broadcast latency: 500-2000ms (high variance) - - Conclusion: Source IP hidden, but Tor analysis possible - - ## Best Practice - - Use Tor + Dandelion++ + varying broadcast timing - - Don't broadcast multiple transactions from same IP quickly - - Consider batching transactions over time - ``` - -### Open a PR This Week - -**Target: bitcoin/bitcoin** - -1. **Navigate to:** - - https://github.com/bitcoin/bitcoin/issues - - Filter by label: "P2P" or "privacy" - -2. **Common contributions:** - - Improve Tor/I2P documentation in bitcoin.conf comments - - Add test cases for Tor peer connection - - Document Dandelion++ research in comments - -3. **Example PR:** - - Improve `bitcoin.conf.example` with detailed Tor/I2P section - - Include examples of `proxy=`, `listen=`, `onlynet=` configuration - - Add privacy implications for each setting - -**OR Target: bitcoin-core/secp256k1 or protocol research** - -- Write research doc on Dandelion++ anonymity analysis -- Create implementation proposal for Dandelion++ in Bitcoin Core - -### Daily Life Privacy Tip - -**Run Bitcoin Core Over Tor Today** - -If you run a node: - -1. **Install Tor:** - ```bash - brew install tor # macOS - sudo apt-get install tor # Ubuntu - ``` - -2. **Edit bitcoin.conf:** - ``` - proxy=127.0.0.1:9050 - listen=1 - onlynet=onion - ``` - -3. **Restart Bitcoin Core** - -4. **Verify:** - ```bash - bitcoin-cli getnetworkinfo | grep "onion\|ipv4" - ``` - -Result: Your transactions are broadcast through Tor. An ISP-level observer can no longer correlate your IP with your transactions. - -### Reading List - -- **Dandelion++ Paper:** https://arxiv.org/abs/1805.11060 -- **Bitcoin Wiki - Tor:** https://en.bitcoin.it/wiki/Tor -- **Bitcoin Core I2P Support:** https://github.com/bitcoin/bitcoin/pull/20685 -- **Tor Project:** https://www.torproject.org -- **I2P Project:** https://geti2p.net -- **Bitcoin Core bitcoin.conf Guide:** https://github.com/bitcoin/bitcoin/blob/master/share/examples/bitcoin.conf -- **Network Surveillance Risks:** https://en.bitcoin.it/wiki/Privacy - ---- - -## Session 14: Compact Block Filters (BIP157/158) - -### Real-World Scenario - -**The Problem:** -You want to use a mobile Bitcoin wallet without running a full node. Traditional SPV (Simple Payment Verification) wallets: -- Connect to a server -- Send all their addresses to the server -- The server responds with relevant transactions -- Result: "The server now knows all of my addresses" - -This is worse than Payjoin or Tor can fix. Your address privacy is compromised at the source. - -**The Solution:** -Compact Block Filters (BIP157/158) let you discover which transactions are yours without revealing which addresses you own: - -1. Full nodes create filter sets for each block (Golomb-Rice coded) -2. Light clients download filters, not addresses -3. Light clients scan filters locally: "Is transaction X relevant to me?" -4. Result: "I see which transactions might be mine, without the server knowing which addresses I own" - -Privacy moves from "trust the server" to "trust the math." - -### Learning Objectives - -- Understand why SPV wallets leak address privacy -- Learn the Golomb-Rice coding scheme for filters -- Understand BIP157 (filter creation) and BIP158 (filter format) -- Build a Golomb-Rice encoder/decoder from scratch -- Implement filter scanning in a light client - -### Privacy Tools You'll Use - -- **Compact Block Filters (BIP157/158)** - filter specification -- **Kyoto** - Rust light client with CBF support -- **Floresta** - Utreexo-based light client -- **Neutrino** (LND) - Lightning client with CBF support -- **Python/Rust** - filter implementation - -### Detailed Hands-On Exercise - -**Part 1: Understand the Privacy Problem** - -1. **Traditional SPV (Breaks Privacy):** - ``` - Light Client Server - ├─ "Do you have txs for:" → - ├─ " - addr1" - ├─ " - addr2" - ├─ " - addr3" - └─ " - addr4" - - Server sees all 4 addresses. - Server now knows: "This person owns these addresses" - ``` - -2. **Compact Block Filters (Preserves Privacy):** - ``` - Full Node Light Client - ├─ Filter(block 1) → - ├─ Filter(block 2) → - ├─ Filter(block 3) → - └─ Filter(block N) → - - Light Client filters locally: - ├─ Check: "Do I have addr1?" - ├─ Check: "Do I have addr2?" - ├─ If "yes", download full block - - Server never learns which addresses you own. - ``` - -3. **Document the trade-off:** - - SPV: Server knows addresses, but small bandwidth - - CBF: Server doesn't know addresses, slightly larger bandwidth (filters) - - Result: Worth the bandwidth increase - -**Part 2: Build a Golomb-Rice Encoder** - -1. **Understand Golomb-Rice coding:** - - Golomb-Rice code is a compression scheme for integers - - Parameter M (2^M determines compression ratio) - - Useful for encoding sparse sets like Bitcoin addresses - -2. **Implement in Python (simple version):** - ```python - def golomb_rice_encode(value, m): - """Encode a single value using Golomb-Rice code""" - q, r = divmod(value, 1 << m) # value // 2^m, value % 2^m - - # Unary code: q ones followed by a zero - unary = '1' * q + '0' - - # Binary code: r in m bits - binary = bin(r)[2:].zfill(m) - - return unary + binary - - def golomb_rice_decode(bitstring, m): - """Decode Golomb-Rice encoded bitstring""" - idx = 0 - - # Count ones (unary part) - q = 0 - while idx < len(bitstring) and bitstring[idx] == '1': - q += 1 - idx += 1 - - # Skip zero separator - idx += 1 - - # Read m bits (binary part) - r = int(bitstring[idx:idx+m], 2) - - return (q << m) + r - - # Test - encoded = golomb_rice_encode(5, 2) # Encode 5 with m=2 - decoded = golomb_rice_decode(encoded, 2) - print(f"Encoded 5: {encoded}") - print(f"Decoded: {decoded}") - assert decoded == 5 - ``` - -3. **Build a set encoder:** - ```python - def encode_set_as_filter(addresses, m=19): - """ - Encode a set of addresses as a Golomb-Rice filter - addresses: list of 32-bit values (hash of addresses) - m: Golomb-Rice parameter (19 is standard for Bitcoin) - """ - # Sort addresses - sorted_addrs = sorted(addresses) - - # Encode differences (more compressible than absolute values) - bitstring = "" - prev = 0 - for addr in sorted_addrs: - diff = addr - prev - bitstring += golomb_rice_encode(diff, m) - prev = addr - - # Convert bitstring to bytes - # Pad to byte boundary - bitstring = bitstring.ljust(((len(bitstring) + 7) // 8) * 8, '0') - - return bytes(int(bitstring[i:i+8], 2) for i in range(0, len(bitstring), 8)) - - # Test - addresses = [100, 200, 300, 1000, 2000] - filter_bytes = encode_set_as_filter(addresses, m=4) - print(f"Filter size: {len(filter_bytes)} bytes for {len(addresses)} addresses") - ``` - -**Part 3: Build a Light Client Filter Scanner** - -1. **Create a simple filter scanner:** - ```python - import hashlib - - def hash_address(address_bytes): - """Hash an address for filter matching""" - return int.from_bytes(hashlib.sha256(address_bytes).digest()[:4], 'big') - - def address_in_filter(address_bytes, filter_bytes, m=19): - """Check if address might be in filter""" - # Hash the address - hashed = hash_address(address_bytes) - - # Decode filter and check - # (This is simplified-real implementation more complex) - filter_values = decode_filter_from_bytes(filter_bytes, m) - - # Check for false positives (expected) - return hashed in filter_values - - # Light client usage: - my_addresses = [b"addr1", b"addr2", b"addr3"] - block_filter = receive_filter_from_node() # Get filter for latest block - - relevant_txns = [] - for address in my_addresses: - if address_in_filter(address, block_filter): - # Download full block to verify (false positive check) - full_block = download_full_block() - for txn in full_block.txns: - if contains_address(txn, address): - relevant_txns.append(txn) - ``` - -**Part 4: Deploy Kyoto or Floresta** - -1. **Try Kyoto (Rust light client):** - ```bash - git clone https://github.com/rustacean-labs/kyoto.git - cd kyoto - cargo build --release - - # Run light client with CBF - ./target/release/kyoto --network testnet --filters - ``` - -2. **Or Floresta (Utreexo-based):** - ```bash - git clone https://github.com/mit-dci/floresta.git - cd floresta - cargo build --release - - ./target/release/floresta-cli - ``` - -3. **Monitor privacy:** - - What data does the server see? (Check network logs) - - What's the bandwidth overhead? - - How long to sync? - -**Part 5: Measure Privacy Properties** - -1. **Create a test comparing:** - - Full node (knows everything, largest bandwidth) - - SPV wallet (leaks addresses, smallest bandwidth) - - CBF light client (hides addresses, medium bandwidth) - -2. **Document:** - ```markdown - # Light Client Privacy Comparison - - | Method | Addresses Leaked | Bandwidth | Sync Time | - |--------|------------------|-----------|-----------| - | Full Node | No | ~3.5 GB | 1-2 days | - | SPV | All | ~50 MB | 1 minute | - | CBF (Kyoto) | No | ~200 MB | 30 minutes | - | Floresta | No | ~100 MB | 20 minutes | - - ## Recommendation - For mobile wallets, CBF (Kyoto/Floresta) is the clear win: - - Full address privacy (like full node) - - Reasonable bandwidth (like SPV) - - Reasonable sync time (between SPV and full node) - ``` - -### Open a PR This Week - -**Target: bitcoindevkit/bdk or lightningdevkit/ldk** - -1. **Navigate to:** - - https://github.com/bitcoindevkit/bdk/issues (filter "privacy") - - https://github.com/lightningdevkit/ldk/issues - -2. **Common contributions:** - - Improve CBF documentation - - Add examples showing privacy properties - - Write tests for filter correctness - - Implement missing filter validation - -3. **Example PR:** - - Add comprehensive example: "Using BDK with Compact Block Filters for Privacy" - - Include code showing filter-based address scanning - - Compare to SPV privacy implications - -### Daily Life Privacy Tip - -**Use a CBF-Based Light Client** - -If you use mobile Bitcoin: - -1. **Install Kyoto** (if available on your platform): - ```bash - # iOS/Android via companion app - # Scans blocks using BIP157/158 - ``` - -2. **Or use Sparrow Wallet** with Private Electrum server - -3. **Why:** - - Your addresses stay private - - Server can't track which coins you own - - Full validation (not "trust the server") - -### Reading List - -- **BIP157 (Client-Side Block Filtering):** https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki -- **BIP158 (Compact Block Filters):** https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki -- **Golomb-Rice Coding:** https://en.wikipedia.org/wiki/Golomb_coding -- **Kyoto:** https://github.com/rustacean-labs/kyoto -- **Floresta:** https://github.com/mit-dci/floresta -- **BDK Documentation:** https://bitcoindevkit.org -- **SPV Privacy Analysis:** https://en.bitcoin.it/wiki/Payment_Verification - ---- - -## Session 15: Light Client Privacy - Floresta & Kyoto - -### Real-World Scenario - -**Your Constraint:** -You can't run a full node on your phone-it requires ~500GB storage and ~10Mbps constant bandwidth. - -**Your Goal:** -You want full validation (don't trust servers) AND address privacy (don't leak which addresses you own). - -**Floresta's Solution:** -Floresta uses Utreexo accumulators to compress the UTXO set from ~10GB to ~600MB. Combined with CBF (from Session 14), it becomes a practical light client: -- Download headers only (~5MB) -- Download filters (~200MB) -- Download relevant blocks -- Use Utreexo proofs to verify UTXO validity without full node state -- Result: "Mobile validation with full privacy" - -### Learning Objectives - -- Understand Utreexo accumulators (tree-based UTXO proofs) -- Deploy Floresta or Kyoto locally -- Query light client for transaction history -- Measure privacy properties vs. SPV -- Contribute to light client ecosystem - -### Privacy Tools You'll Use - -- **Floresta** - Utreexo-based light client (Rust) -- **Kyoto** - Header-based light client with CBF (Rust) -- **Bitcoin Core** - full node for Utreexo bridge -- **bitcoin-cli** - validation - -### Detailed Hands-On Exercise - -**Part 1: Understand Utreexo** - -1. **The Problem Utreexo Solves:** - - Full nodes maintain UTXO set (~10GB, grows over time) - - Light clients need to verify "Is this UTXO valid?" without storing entire set - - Traditional: Download full UTXO set (impractical) - -2. **The Utreexo Solution:** - - Accumulator: Hash-based proof that UTXO exists without storing all UTXOs - - Proof size: ~600 bytes per UTXO - - Verification: Fast (O(log N) operations) - -3. **Create a diagram:** - ``` - Full Node UTXO Set (10GB): - {UTXO1, UTXO2, UTXO3, ..., UTXO1000000} - - Utreexo Accumulator: - └─ Root Hash (32 bytes) - └─ Proof for UTXO42 (600 bytes) - └─ Proof for UTXO999 (600 bytes) - └─ Proof for UTXO5000 (600 bytes) - - Light client can verify: "UTXO42 is valid" - Without storing the full 10GB - ``` - -**Part 2: Deploy Floresta** - -1. **Clone and build:** - ```bash - git clone https://github.com/mit-dci/floresta.git - cd floresta - cargo build --release --bin floresta-cli - ``` - -2. **Configure Floresta:** - Create `floresta.toml`: - ```toml - [network] - network = "testnet" # Use testnet first - - [filters] - use_compact_filters = true - filter_cache_dir = "./filter_cache" - - [storage] - datadir = "./floresta_data" - assume_utreexo = false # Set true if you trust genesis UTXO set - ``` - -3. **Start Floresta:** - ```bash - ./target/release/floresta-cli - ``` - -4. **Query wallet:** - ```bash - # In Floresta CLI: - > sync_status - > get_balance - > list_unspent - ``` - -**Part 3: Test Light Client Privacy** - -1. **Create test addresses:** - ```bash - # In Floresta - > getnewaddress - > getnewaddress - > getnewaddress - ``` - -2. **Have testnet coins sent to your addresses** (use faucet) - -3. **Monitor network traffic:** - ```bash - # Terminal 1: Floresta - ./target/release/floresta-cli - - # Terminal 2: Monitor DNS/network - # macOS - nettop -p floresta-cli - - # Linux - tcpdump -i any -n 'tcp port 8333 or 8334' - ``` - -4. **Observe:** - - What data leaves your device? - - Does the server learn your addresses? - - What blocks are downloaded? - -5. **Document findings:** - ```markdown - # Floresta Privacy Test - - Test Environment: Testnet, 1 hour usage - - ## What Server Sees - - Block headers: Yes (unavoidable) - - Filter requests: Yes (normal) - - Address list: NO (Floresta hides) - - Transaction timing: Partially (you request blocks) - - ## What Server Doesn't See - - Which specific addresses you own - - Full transaction history (only relevant blocks) - - Balance totals - - Spending patterns - - ## Conclusion - Floresta provides address privacy superior to SPV, - comparable to full node, with mobile-friendly bandwidth. - ``` - -**Part 4: Compare Implementations** - -Create a comparison table: - -| Property | Full Node | SPV | Kyoto (CBF) | Floresta (Utreexo) | -|----------|-----------|-----|-------------|-------------------| -| Bandwidth (monthly) | 5 GB | 50 MB | 200 MB | 100 MB | -| Storage | 500 GB+ | 100 MB | 2 GB | 1 GB | -| Validation | Full | Weak | Full (headers) | Full (UTXO proofs) | -| Address Privacy | Yes | No | Yes | Yes | -| Sync Time | 1-2 days | 5 minutes | 2 hours | 1 hour | -| Mobile-Friendly | No | Yes | Partial | Yes | - -### Open a PR This Week - -**Target: mit-dci/floresta or rustacean-labs/kyoto** - -1. **Navigate to:** - - https://github.com/mit-dci/floresta/issues - - https://github.com/rustacean-labs/kyoto/issues - -2. **Common contributions:** - - Improve documentation (setup guide, troubleshooting) - - Write tests for Utreexo proof validation - - Add examples showing privacy properties - - Improve error messages - -3. **Example PR:** - - Write "Floresta Mobile Setup Guide" with screenshots - - Include privacy properties explanation - - Add troubleshooting section - -### Daily Life Privacy Tip - -**Deploy Floresta on a Home Server** - -If you have a Raspberry Pi or old laptop: - -1. **Build Floresta:** - ```bash - git clone https://github.com/mit-dci/floresta.git - cd floresta - cargo build --release - ``` - -2. **Run as daemon:** - ```bash - ./target/release/floresta-cli > floresta.log 2>&1 & - ``` - -3. **Connect mobile wallet:** - - Point wallet to your home server - - Syncs with full privacy - - You own the infrastructure - -### Reading List - -- **Floresta GitHub:** https://github.com/mit-dci/floresta -- **Kyoto GitHub:** https://github.com/rustacean-labs/kyoto -- **Utreexo Paper:** https://eprint.iacr.org/2019/611.pdf -- **Compact Block Filters (BIP157/158):** https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki -- **Bitcoin Core Utreexo Integration:** https://github.com/bitcoin/bitcoin/pull/27050 -- **Light Client Privacy Primer:** https://en.bitcoin.it/wiki/Lightweight_client - ---- - -## Session 16: Taproot Privacy - Schnorr, MAST, MuSig2 - -### Real-World Scenario - -**The Visual Problem:** -Alice, Bob, and Carol are business partners sharing a 3-of-3 multisig. On-chain, it looks obviously like a multisig: -``` -Script: OP_3 OP_3 OP_CHECKMULTISIG -Size: ~100 bytes (bloated) -Hashing: Recursive (expensive) -``` - -A chain analyst sees this and immediately knows: "Three people control this address" or "This is a smart contract." - -**Taproot's Solution:** -With Taproot, the same 3-of-3 multisig: -1. Uses a "key spend" path (MuSig2): All 3 parties cooperatively spend using a single Schnorr signature -2. The scriptpubkey is just a Schnorr key (32 bytes, identical to single-sig) -3. On-chain: Indistinguishable from a single-signature payment -4. Result: "The 3-of-3 multisig looks identical to a plain payment. It's in the largest anonymity set on Bitcoin." - -### Learning Objectives - -- Understand Schnorr signatures (BIP340) and why they're better than ECDSA -- Learn Taproot (BIP341) and its privacy implications -- Understand MuSig2 (BIP327) for aggregated signatures -- Build Taproot addresses from scratch -- Use P2TR addresses for maximum privacy - -### Privacy Tools You'll Use - -- **bitcoin-cli** - create Taproot addresses and transactions -- **Sparrow Wallet** - full Taproot support -- **bitcoinjs-lib** (JavaScript) - Taproot construction -- **rust-bitcoin** - Taproot primitives -- **libsecp256k1-zkp** - Schnorr signatures - -### Detailed Hands-On Exercise - -**Part 1: Understand Schnorr Signatures** - -1. **Why Schnorr > ECDSA for Privacy:** - - **ECDSA (Bitcoin legacy):** - - Signature: (r, s) - two 32-byte values - - Different signatures for same key/message (randomness in signing) - - Non-linear signature equation (harder to aggregate) - - **Schnorr (Taproot):** - - Signature: (R, s) - one 32-byte point + one 32-byte scalar - - Deterministic signatures (same input → same signature) - - Linear equation (easy to aggregate multiple signatures) - -2. **Create a comparison table:** - ``` - | Property | ECDSA | Schnorr | - |----------|-------|---------| - | Signature size | 72 bytes | 64 bytes | - | Aggregation | Impossible | BIP327 (MuSig2) | - | Hashing | Legacy (SigHash) | Modern (BIP341) | - | Privacy | Reveals key type | Hidden script | - | Adoption | Legacy | Taproot | - ``` - -3. **Understand the math** (simplified): - ``` - ECDSA signature: (r, s) - where s = k^-1 (h + r*x) mod n - (k is random, so different s each time) - - Schnorr signature: (R, s) - where s = k + h*x mod n - where R is the public point k*G - (k is random, but signature is deterministic in x) - - Key insight: With Schnorr, you can aggregate: - sig1 = k1 + h*x1 - sig2 = k2 + h*x2 - combined_sig = (k1+k2) + h*(x1+x2) = k_combined + h*x_combined - (This is impossible with ECDSA) - ``` - -**Part 2: Build Taproot Addresses** - -1. **Using bitcoin-cli (testnet):** - ```bash - # Start Bitcoin Core on testnet - bitcoind -testnet -server - - # Create a new Taproot address - bitcoin-cli -testnet createwallet test_taproot - bitcoin-cli -testnet -rpcwallet=test_taproot getnewaddress "" bech32m - - # Verify it's P2TR (starts with 'tb1p' on testnet) - # Example: tb1pzzr4m4gts9d3j5qvpwd6qw8mvqxlgx7mlhpfchqwyz8p4wc65lgsx3lnsm - ``` - -2. **Using Sparrow Wallet (GUI):** - - Open Sparrow - - Create new wallet - - Settings → Script type → P2TR (Pay to Taproot) - - Generate address - - All addresses are now Taproot - -3. **Using rust-bitcoin (programmatic):** - ```rust - use bitcoin::PublicKey; - use bitcoin::taproot::TaprootBuilder; - use bitcoin::key::Secp256k1; - - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng()); - - // Create a simple Taproot key-only address - let builder = TaprootBuilder::new().finalize(&secp, public_key).unwrap(); - let taproot_address = bitcoin::Address::p2tr(&secp, public_key, None, bitcoin::Network::Testnet); - - println!("Taproot address: {}", taproot_address); - ``` - -**Part 3: Understand Taproot Privacy** - -1. **Script Types on Bitcoin:** - - **Pre-Taproot:** - ``` - P2PKH: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG - P2SH: OP_HASH160 OP_EQUAL - P2WPKH: (witness) - P2WSH: (witness) - ``` - - All different sizes/formats - - Analyst can see: "This is multisig" vs "This is single-sig" vs "This is contract" - - **Taproot (P2TR):** - ``` - OP_1 <32-byte-taproot-key> - ``` - - Everything looks identical - - Single-sig payment? Could be - - Multisig? Could be - - Smart contract? Could be - - Analyst can't tell - -2. **Anonymity Set:** - - Pre-Taproot: Anonymity set = same script type - - Taproot: Anonymity set = ALL Taproot transactions (and growing) - - Benefit: Massive privacy improvement - -3. **Create a visualization:** - ``` - On-chain (2024): - ├─ P2PKH: 25% (old, declining) - ├─ P2SH: 20% (old, declining) - ├─ P2WPKH: 40% (SegWit, standard) - ├─ P2WSH: 5% (SegWit, multisig) - ├─ P2TR: 10% (Taproot, growing) - └─ Other: <1% - - Taproot privacy benefit: - - Old P2PKH multisig: Anonymity set = P2SH (20% of transactions) - - New P2TR multisig: Anonymity set = P2TR (10% now, >50% future) - - By 2026, P2TR will be >50% of transactions - - All P2TR transactions will look identical on-chain - ``` - -**Part 4: Create Taproot Multisig (MuSig2)** - -1. **Conceptual: How MuSig2 Works** - ``` - Alice, Bob, Carol want to create 3-of-3 multisig - - Old way (P2WSH): - - Alice creates pubkey_alice - - Bob creates pubkey_bob - - Carol creates pubkey_carol - - Script: OP_3 pubkey_alice pubkey_bob pubkey_carol OP_3 OP_CHECKMULTISIG - - On-chain: ~100 bytes, obviously multisig - - New way (Taproot + MuSig2): - - Alice, Bob, Carol exchange pubkeys - - Collaborate to compute aggregate_pubkey = MuSig2(pubkey_alice, pubkey_bob, pubkey_carol) - - Taproot address: P2TR(aggregate_pubkey) - - On-chain: 34 bytes, indistinguishable from single-sig - - To spend: All 3 collaborate to create 1 Schnorr signature - - Result: Looks like a normal payment on-chain - ``` - -2. **Using Sparrow (practical):** - - Sparrow doesn't yet have full MuSig2 UI, but work is underway - - For now: Use single-sig P2TR addresses (same privacy benefit) - -3. **Using rust-bitcoin (advanced):** - ```rust - // MuSig2 requires secp256k1-zkp (not yet in rust-bitcoin) - // Check: https://github.com/blockstream/secp256k1-zkp - // For now, prefer single-sig P2TR - ``` - -**Part 5: Measure Taproot Impact** - -1. **Analyze blockchain:** - ```bash - # Count Taproot transactions in recent blocks - bitcoin-cli -testnet getblockcount - for i in {0..100}; do - block=$(bitcoin-cli -testnet getblockhash $(bitcoin-cli -testnet getblockcount | jq '. - 100 + '$i)) - bitcoin-cli -testnet getblock $block | grep -c '"version": 1' # P2TR outputs - done - ``` - -2. **Create a document:** - ```markdown - # Taproot Privacy Analysis - - ## Current State (2026) - - P2TR adoption: ~25% of UTXOs - - Anonymity set: All P2TR looks identical - - ## Single-Sig P2TR - - Address: P2TR(single_pubkey) - - Cost: 34 bytes (smaller than P2WPKH!) - - Privacy: In anonymity set with all other P2TR - - ## Multisig P2TR (MuSig2) - - Address: P2TR(aggregate_pubkey) - - Cost: 34 bytes (vs 100+ for P2WSH) - - Privacy: Identical to single-sig on-chain - - Assumption: User who controls multisig is unknown - - ## Recommendation - All new addresses should use P2TR for: - - Maximum privacy - - Smaller on-chain footprint - - Better signature aggregation (future) - ``` - -### Open a PR This Week - -**Target: sparrowwallet/sparrow or bitcoin-core/secp256k1** - -1. **For Sparrow:** - - Improve Taproot address display - - Add documentation: "Why use P2TR?" - - Create migration guide from P2WPKH → P2TR - -2. **For secp256k1:** - - Improve Schnorr signature documentation - - Add examples of signature aggregation concepts - -3. **Example PR:** - - Write comprehensive Taproot guide for Sparrow documentation - - Include privacy implications - - Show address creation steps - - Compare script types visually - -### Daily Life Privacy Tip - -**Use P2TR Addresses Exclusively** - -Effective immediately: - -1. **Generate only P2TR addresses:** - - Sparrow: Settings → Script type → P2TR - - Bitcoin Core: `getnewaddress "" bech32m` - - Bull Bitcoin: Automatically P2TR - -2. **Why:** - - Smallest anonymity set (and growing) - - Smallest on-chain footprint - - Best future-proofing (MuSig2 support coming) - -3. **Migration:** - - Keep old addresses for receiving - - Consolidate to P2TR in future Coinjoin - -### Reading List - -- **BIP340 (Schnorr Signatures):** https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki -- **BIP341 (Taproot):** https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki -- **BIP327 (MuSig2):** https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki -- **Taproot Privacy Analysis:** https://arxiv.org/pdf/2201.07569.pdf -- **Sparrow Wallet Taproot Support:** https://sparrowwallet.com -- **rust-bitcoin Taproot:** https://docs.rs/bitcoin/latest/bitcoin/taproot/ -- **Bitcoin Script Improvements:** https://bitcoin-dev.wiki/w/index.php/Taproot - ---- - -## Phase 4 Summary & Next Steps - -**What You've Achieved:** -1. Secured transaction broadcast (Tor, I2P, Dandelion++ concepts) -2. Built address discovery privacy (Compact Block Filters) -3. Deployed light clients (Floresta, Kyoto) -4. Maximized script privacy (Taproot, MuSig2) - -**Privacy Layers (Integrated):** -- Transaction: Payjoin (Phase 3) + Taproot (Phase 4) = indistinguishable outputs -- Network: Tor/I2P + Dandelion++ = hidden broadcast origin -- Address: CBF filters + Light clients = no server knows your addresses -- Script: P2TR + MuSig2 = identical to single-sig on-chain - -**You've Now Learned:** -- Phase 1: UTXO Management (coin selection, consolidation) -- Phase 2: CoinJoin (mixing, Wasabi, coordination) -- Phase 3: Payjoin (sender/receiver cooperation) -- Phase 4: Network & Protocol Privacy (broadcast, addresses, scripts) - -**The Real-World Impact:** -A Bitcoiner who implements all four phases becomes: -- Immune to CIOH analysis (Payjoin) -- Invisible to network surveillance (Tor) -- Hidden from address discovery (CBF) -- Indistinguishable in the largest anonymity set (Taproot) - -**Your Next Challenge:** -The best privacy tool is adoption. Your mission now: Build, deploy, and teach others the tools you've learned. Privacy is not a feature-it's a human right. diff --git a/privacy-track/phase-4-network-privacy/session-14-compact-block-filters/compact_block_filters.py b/privacy-track/phase-4-network-privacy/session-14-compact-block-filters/compact_block_filters.py deleted file mode 100644 index 453f65c..0000000 --- a/privacy-track/phase-4-network-privacy/session-14-compact-block-filters/compact_block_filters.py +++ /dev/null @@ -1,518 +0,0 @@ -""" -Bitcoin Privacy Track — Module 4, Session 2: Compact Block Filters -Code Orange Dev School | codeorange.dev - -Implement a simplified compact block filter (BIP158) from scratch, -then use it to understand how light clients can scan for transactions -privately. - -This is critical infrastructure for Silent Payments on mobile devices. - -Prerequisites: -- Understanding of hash functions and probabilistic data structures -- Completed Module 2 (Silent Payments scanning) -- Python 3.8+ - -References: -- BIP158: https://bips.dev/158/ -- Golomb-Rice Coding: https://en.wikipedia.org/wiki/Golomb_coding - -Instructions: -- Complete each function by filling in the code where indicated -- Run: python3 compact_block_filters.py -- Bring your results to the weekly session -""" - -import hashlib -import struct -import math -import secrets -import time -from typing import List, Set, Optional -from io import BytesIO - - -# ============================================================ -# Helpers (provided) -# ============================================================ - -def siphash(key: bytes, data: bytes) -> int: - """SipHash-2-4 — the hash function used in BIP158 filters. - - For simplicity, we use a SHA256-based simulation. - Real BIP158 uses SipHash-2-4 (much faster). - """ - h = hashlib.sha256(key + data).digest() - return int.from_bytes(h[:8], 'little') - - -def hash_to_range(item: bytes, f: int, key: bytes) -> int: - """Hash an item to a value in [0, f). - - This maps each item uniformly into the filter's range, - which is N * M where N = number of items and M = 784931 (BIP158). - - Args: - item: the data element to hash - f: the range upper bound (N * M) - key: the hash key (derived from block hash) - - Returns: - integer in [0, f) - """ - h = siphash(key, item) - return (h * f) >> 64 if f < (1 << 64) else h % f - - -# ============================================================ -# BIP158 Parameters -# ============================================================ - -# BIP158 basic filter parameters -P = 19 # Golomb-Rice parameter (2^19 = 524288 — the false positive rate denominator) -M = 784931 # M ≈ 1.497137 * 2^P — optimized for minimum filter size - - -# ============================================================ -# Exercise 1: Golomb-Rice Encoding -# ============================================================ - -class BitWriter: - """Write individual bits to a byte buffer.""" - def __init__(self): - self.data = bytearray() - self.current_byte = 0 - self.bit_position = 0 # 0-7, MSB first - - def write_bit(self, bit: int): - """Write a single bit (0 or 1).""" - if bit: - self.current_byte |= (1 << (7 - self.bit_position)) - self.bit_position += 1 - if self.bit_position == 8: - self.data.append(self.current_byte) - self.current_byte = 0 - self.bit_position = 0 - - def write_bits(self, value: int, num_bits: int): - """Write num_bits bits from value (MSB first).""" - for i in range(num_bits - 1, -1, -1): - self.write_bit((value >> i) & 1) - - def flush(self): - """Flush remaining bits (pad with zeros).""" - if self.bit_position > 0: - self.data.append(self.current_byte) - self.current_byte = 0 - self.bit_position = 0 - - def get_bytes(self) -> bytes: - self.flush() - return bytes(self.data) - - -class BitReader: - """Read individual bits from a byte buffer.""" - def __init__(self, data: bytes): - self.data = data - self.byte_index = 0 - self.bit_position = 0 # 0-7, MSB first - - def read_bit(self) -> int: - """Read a single bit.""" - if self.byte_index >= len(self.data): - raise EOFError("No more bits to read") - bit = (self.data[self.byte_index] >> (7 - self.bit_position)) & 1 - self.bit_position += 1 - if self.bit_position == 8: - self.byte_index += 1 - self.bit_position = 0 - return bit - - def read_bits(self, num_bits: int) -> int: - """Read num_bits bits and return as integer.""" - value = 0 - for _ in range(num_bits): - value = (value << 1) | self.read_bit() - return value - - -def golomb_rice_encode(values: List[int], p: int = P) -> bytes: - """Encode a sorted list of deltas using Golomb-Rice coding. - - Golomb-Rice coding splits each value into: - - quotient q = value >> p (encoded as q ones followed by a zero) - - remainder r = value & ((1 << p) - 1) (encoded as p bits) - - Args: - values: list of non-negative integers (sorted deltas) - p: Golomb-Rice parameter (number of bits for remainder) - - Returns: - compressed bytes - """ - # YOUR CODE HERE - # - # For each value in the list: - # 1. Compute quotient: q = value >> p - # 2. Compute remainder: r = value & ((1 << p) - 1) - # 3. Write q ones followed by a zero (unary encoding of q) - # Example: q=3 → write bits 1, 1, 1, 0 - # 4. Write r as p bits (binary encoding) - # - # Use the BitWriter class to write bits - # Return writer.get_bytes() - - pass - - -def golomb_rice_decode(data: bytes, n: int, p: int = P) -> List[int]: - """Decode n Golomb-Rice coded values from data. - - Args: - data: compressed bytes - n: number of values to decode - p: Golomb-Rice parameter - - Returns: - list of decoded values - """ - # YOUR CODE HERE - # - # For each of n values: - # 1. Read unary-encoded quotient: - # Count consecutive 1-bits until you hit a 0 - # q = count of 1-bits - # 2. Read p bits for the remainder r - # 3. value = (q << p) | r - # - # Use the BitReader class to read bits - # Return list of decoded values - - pass - - -# ============================================================ -# Exercise 2: Build a Compact Block Filter -# ============================================================ - -def build_filter(elements: List[bytes], block_hash: bytes) -> bytes: - """Build a BIP158-style compact block filter. - - The filter contains all scriptPubKeys from the block's outputs - and spent outputs. A light client can test whether their - scriptPubKey appears in the filter WITHOUT downloading the block. - - Args: - elements: list of scriptPubKey bytes to include in the filter - block_hash: the block hash (used to derive the hash key) - - Returns: - the compact block filter as bytes - """ - # YOUR CODE HERE - # - # Algorithm: - # 1. N = number of elements - # 2. F = N * M (the hash range) - # 3. key = block_hash[:16] (first 16 bytes as SipHash key) - # - # 4. Hash each element to a value in [0, F): - # hashed_values = [hash_to_range(elem, F, key) for elem in elements] - # - # 5. Sort the hashed values - # - # 6. Compute deltas (differences between consecutive sorted values): - # deltas[0] = sorted_values[0] - # deltas[i] = sorted_values[i] - sorted_values[i-1] - # - # 7. Golomb-Rice encode the deltas - # - # 8. Prepend N as a varint (CompactSize encoding): - # if N < 253: 1 byte - # if N < 65536: 0xfd + 2 bytes little-endian - # etc. - # - # 9. Return N_bytes + encoded_deltas - - pass - - -# ============================================================ -# Exercise 3: Query the Filter -# ============================================================ - -def match_filter(filter_data: bytes, query_elements: List[bytes], - block_hash: bytes) -> bool: - """Test if any of the query elements might be in the filter. - - This is what a light client does: given a compact block filter - and a set of scriptPubKeys it's watching, determine if this - block might contain a relevant transaction. - - Args: - filter_data: the compact block filter bytes - query_elements: list of scriptPubKeys to search for - block_hash: the block hash (for deriving the hash key) - - Returns: - True if any query element matches (may be a false positive!) - False if definitely no match - """ - # YOUR CODE HERE - # - # Algorithm: - # 1. Parse N from the filter data (varint) - # 2. F = N * M - # 3. key = block_hash[:16] - # - # 4. Hash each query element to [0, F): - # query_values = sorted([hash_to_range(q, F, key) for q in query_elements]) - # - # 5. Decode the filter to get the sorted set of hashed values - # (decode the Golomb-Rice deltas and compute cumulative sums) - # - # 6. Check if any query_value appears in the filter's sorted set - # Use a sorted merge (two-pointer technique) for efficiency: - # - Pointer i through filter values, pointer j through query values - # - If filter[i] == query[j]: MATCH → return True - # - If filter[i] < query[j]: advance i - # - If filter[i] > query[j]: advance j - # - # 7. If no match found → return False - - pass - - -# ============================================================ -# Exercise 4: Measure Filter Properties -# ============================================================ - -def analyze_filter_properties(): - """Measure the size, false positive rate, and query time of filters. - - Simulates realistic block data and measures key properties. - """ - print("\n" + "=" * 60) - print("FILTER ANALYSIS") - print("=" * 60) - - # Simulate a block with N unique scriptPubKeys - sizes = [100, 500, 1000, 2500, 5000] - - for n_elements in sizes: - # Generate random "scriptPubKeys" - elements = [secrets.token_bytes(32) for _ in range(n_elements)] - block_hash = secrets.token_bytes(32) - - # Build filter - start = time.time() - filter_data = build_filter(elements, block_hash) - build_time = time.time() - start - - if filter_data is None: - print(f"\n {n_elements} elements: build_filter() not implemented") - continue - - filter_size = len(filter_data) - raw_size = n_elements * 32 # 32 bytes per scriptPubKey - - # Measure query time - # Test with elements that ARE in the filter (should all match) - true_positives = 0 - sample_size = min(100, n_elements) - query_sample = elements[:sample_size] - - start = time.time() - for q in query_sample: - if match_filter(filter_data, [q], block_hash): - true_positives += 1 - query_time = time.time() - start - - # Measure false positive rate - # Test with elements NOT in the filter - false_positives = 0 - fp_tests = 10000 - for _ in range(fp_tests): - fake = secrets.token_bytes(32) - if match_filter(filter_data, [fake], block_hash): - false_positives += 1 - - fp_rate = false_positives / fp_tests - theoretical_fp = 1 / M # BIP158's theoretical FP rate - - print(f"\n Block with {n_elements} scriptPubKeys:") - print(f" Filter size: {filter_size:,} bytes ({filter_size/raw_size*100:.1f}% of raw)") - print(f" Compression: {raw_size/filter_size:.1f}x") - print(f" Build time: {build_time*1000:.1f} ms") - print(f" Query time: {query_time/sample_size*1000:.3f} ms per element") - print(f" True positives: {true_positives}/{sample_size} ({true_positives/sample_size*100:.0f}%)") - print(f" False positives: {false_positives}/{fp_tests} ({fp_rate*100:.4f}%)") - print(f" Theoretical FP: {theoretical_fp*100:.4f}%") - - # YOUR CODE HERE: Answer these questions - # - # 1. How does filter size scale with the number of elements? - # (Linear? Sub-linear? What's the per-element cost?) - # - # 2. Mainnet averages ~3000 outputs per block. What would the - # filter size be? Compare to the full block size (~1.5 MB). - # - # 3. If a wallet watches 100 addresses, how many false positive - # block downloads per day? (Mainnet: ~144 blocks/day) - # - # 4. How does this compare to BIP37 Bloom filters in terms of privacy? - # With Bloom filters, the server knows which addresses you're watching. - # With compact block filters, the server knows nothing. - - pass - - -# ============================================================ -# Exercise 5: Silent Payments + Compact Block Filters -# ============================================================ - -def simulate_sp_scanning_with_filters(): - """Simulate how compact block filters improve Silent Payment scanning. - - Without filters: scan every transaction in every block (ECDH per tx) - With filters: first check filter, only scan matching blocks - - This is the key optimization that makes Silent Payments viable - on mobile devices. - """ - print("\n" + "=" * 60) - print("SILENT PAYMENTS + COMPACT BLOCK FILTERS") - print("=" * 60) - - # Simulate 144 blocks (1 day of mainnet) - num_blocks = 144 - txs_per_block = 3000 - ecdh_time_ms = 0.5 # ~0.5ms per ECDH operation (mobile device estimate) - - # Without filters: scan everything - total_txs = num_blocks * txs_per_block - scan_time_no_filter = total_txs * ecdh_time_ms / 1000 # seconds - - print(f"\n Scenario: {num_blocks} blocks, {txs_per_block} tx/block") - print(f" Total transactions: {total_txs:,}") - print(f" ECDH time estimate: {ecdh_time_ms} ms/operation") - - print(f"\n WITHOUT filters:") - print(f" ECDH operations: {total_txs:,}") - print(f" Scan time: {scan_time_no_filter:.0f} seconds ({scan_time_no_filter/60:.1f} minutes)") - - # With filters: check filter first, only download matching blocks - # A filter match means "this block MIGHT have your payment" - # False positive rate determines how many extra blocks you download - - # YOUR CODE HERE - # - # Calculate: - # 1. filter_download_size = num_blocks * estimated_filter_size_per_block - # (Use your analysis from Exercise 4 for a 3000-element block) - # - # 2. Expected false positive blocks per day: - # Each block filter has FP rate ≈ 1/M ≈ 1/784931 - # But the receiver is checking against their tweaked output key - # Expected matches = num_blocks * (1/M) - # Add 1 for the actual payment block - # - # 3. For matching blocks only, scan all transactions (ECDH per tx) - # matching_blocks = expected_false_positives + 1 - # scan_time_with_filter = matching_blocks * txs_per_block * ecdh_time_ms / 1000 - # - # 4. Total time = filter_check_time + scan_time_with_filter - # filter_check_time is negligible (just bit matching, no ECDH) - # - # 5. Print the comparison: - # Speedup = scan_time_no_filter / total_time_with_filter - # - # QUESTION: Is the filtered scanning time acceptable for a mobile wallet - # that syncs once per day? What about once per hour? - - pass - - -# ============================================================ -# Run all exercises -# ============================================================ - -if __name__ == "__main__": - print("=" * 70) - print("Bitcoin Privacy Track — Compact Block Filters") - print("Code Orange Dev School | codeorange.dev") - print("=" * 70) - - # Exercise 1: Test Golomb-Rice coding - print("\nExercise 1: Golomb-Rice Encoding/Decoding") - test_values = [5, 12, 3, 100, 0, 7, 42, 1, 88, 15] - encoded = golomb_rice_encode(test_values) - if encoded is None: - print(" golomb_rice_encode() not implemented yet") - else: - decoded = golomb_rice_decode(encoded, len(test_values)) - if decoded is None: - print(" golomb_rice_decode() not implemented yet") - else: - match = decoded == test_values - print(f" Original: {test_values}") - print(f" Decoded: {decoded}") - print(f" Match: {'PASS' if match else 'FAIL'}") - print(f" Raw size: {len(test_values) * 4} bytes (32-bit ints)") - print(f" Encoded: {len(encoded)} bytes") - print(f" Ratio: {len(encoded) / (len(test_values) * 4) * 100:.0f}%") - - # Exercise 2-3: Build and query a filter - print("\nExercise 2-3: Build and Query Filter") - elements = [f"scriptpubkey_{i}".encode() for i in range(100)] - block_hash = hashlib.sha256(b"test_block").digest() - - filter_data = build_filter(elements, block_hash) - if filter_data is None: - print(" build_filter() not implemented yet") - else: - print(f" Filter built: {len(filter_data)} bytes for {len(elements)} elements") - - # Test: element IN the filter - found = match_filter(filter_data, [elements[42]], block_hash) - print(f" Query known element: {'MATCH (correct)' if found else 'NO MATCH (bug!)'}") - - # Test: element NOT in the filter - found = match_filter(filter_data, [b"not_in_filter"], block_hash) - print(f" Query unknown element: {'FALSE POSITIVE' if found else 'NO MATCH (correct)'}") - - # Exercise 4: Filter analysis - print("\nExercise 4: Filter Properties") - analyze_filter_properties() - - # Exercise 5: SP + CBF simulation - print("\nExercise 5: Silent Payments + Compact Block Filters") - simulate_sp_scanning_with_filters() - - print("\n" + "=" * 70) - print("REFLECTION QUESTIONS:") - print("=" * 70) - print(""" - 1. Why does BIP158 use Golomb-Rice coding instead of simpler - compression? What property of the delta values makes GR efficient? - - 2. If you increase the parameter P (and thus M), what happens to: - a) Filter size? - b) False positive rate? - c) Number of blocks a light client downloads unnecessarily? - - 3. A malicious full node could send you a WRONG filter (omitting - your transaction). How does BIP157 handle this? - Hint: filter headers form a chain, and you can ask multiple peers. - - 4. For Silent Payments specifically, the "query element" changes - for every block (because it depends on the sender's inputs). - How does this affect the filter-checking approach? - - 5. Could compact block filters be used to improve Payjoin receiver - efficiency? Why or why not? - """) - print("=" * 70) - print("Done! Bring your results to the weekly session.") - print("=" * 70) diff --git a/privacy-track/phase-4-network-privacy/session-16-taproot-privacy/taproot_privacy_lab.py b/privacy-track/phase-4-network-privacy/session-16-taproot-privacy/taproot_privacy_lab.py deleted file mode 100644 index 5550b5d..0000000 --- a/privacy-track/phase-4-network-privacy/session-16-taproot-privacy/taproot_privacy_lab.py +++ /dev/null @@ -1,553 +0,0 @@ -""" -Taproot Privacy Analysis Lab: Evaluating Privacy Improvements of SegWit v1 -=========================================================================== - -In this exercise, you will learn: -1. How Taproot (BIP 341) improves Bitcoin privacy on-chain -2. Why uniform output sizes enhance privacy -3. How key-path spending is more efficient than script-path -4. Why distinguishability of transaction types reduces anonymity -5. How CoinJoin becomes more efficient with CISA (Cross-Input Signature Aggregation) -6. How to measure Taproot adoption and its privacy implications - -Taproot Context: -- Bitcoin address format: P2PK, P2PKH, P2SH, P2WPKH (SegWit v0), P2WSH, P2TR (SegWit v1) -- SegWit v0 (2016): Separated signatures from transaction data (reduced on-chain footprint) -- Taproot/SegWit v1 (2021): Unified all types into uniform output format - -Privacy Benefits of Taproot: -1. UNIFORM OUTPUTS: All Taproot outputs (P2TR) are 32 bytes - - Old: P2PKH=25 bytes, P2SH=20 bytes, P2WPKH=22 bytes - easily distinguished - - New: P2TR=32 bytes - identical for key-path and script-path spends - - Benefit: Observer cannot distinguish spend type - -2. EFFICIENT KEY-PATH SPENDING: Default Taproot spend is key-path aggregate - - Signature: 64 bytes (vs 71-73 bytes for ECDSA) - - Witness witness data smaller - - Benefit: Honest users pay less for privacy (key-path is default/cheapest) - -3. SCRIPT-PATH FLEXIBILITY: Taproot scripts are more flexible - - Supports multiple keys with lower witness overhead - - Benefit: Multi-sig becomes cheaper, encouraging its use for privacy - -4. CISA POTENTIAL: Cross-Input Signature Aggregation (not yet deployed) - - CoinJoin with n participants could have 1 signature instead of n - - Massive fee savings and scalability improvement - - Would make privacy much more accessible - -Why This Matters for Privacy: -- Transaction surveillance is a real threat to Bitcoin privacy -- Heuristics can link addresses: "change address", "same-input-outputs", etc. -- If spend types are distinguishable, attacker knows your transaction structure -- Uniform outputs limit attacker's ability to identify transaction types -- Larger Taproot adoption increases anonymity set (you blend in better) - -This exercise simulates transaction data. Real analysis uses blockchain APIs -like blockchain.info, mempool.space, or Glassnode to track actual transactions. -""" - -from typing import List, Dict, Tuple -from collections import Counter -import json - - -# ============================================================================ -# Sample Transaction Data -# ============================================================================ - -# Each transaction has: -# - type: Address format (P2PKH, P2SH-P2WPKH, P2WPKH, P2WSH, P2TR key-path, P2TR script-path) -# - witness_size: Bytes of witness data (includes signatures) -# - scriptPubKey_size: Bytes of output script (what goes on chain) -# - on_chain_distinguishable: Can observer identify the type from on-chain data? - -SAMPLE_TX_TYPES = [ - { - 'type': 'P2PKH', - 'witness_size': 0, # P2PKH has no witness (pre-SegWit) - 'scriptPubKey_size': 25, # OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG - 'on_chain_distinguishable': True, - 'typical_input_size': 148, # bytes - 'description': 'Pay to Public Key Hash (Legacy, pre-2015)' - }, - { - 'type': 'P2SH-P2WPKH', - 'witness_size': 108, # 1 signature + 1 pubkey - 'scriptPubKey_size': 20, # OP_HASH160 OP_EQUAL - 'on_chain_distinguishable': True, - 'typical_input_size': 23 + 108, - 'description': 'P2SH-wrapped SegWit v0 (Segwit compatibility mode, 2016-2017)' - }, - { - 'type': 'P2WPKH', - 'witness_size': 108, # 1 signature + 1 pubkey - 'scriptPubKey_size': 22, # OP_0 - 'on_chain_distinguishable': True, - 'typical_input_size': 22 + 108, - 'description': 'Native SegWit v0: Pay to Witness Public Key Hash (2016-present)' - }, - { - 'type': 'P2WSH', - 'witness_size': 180, # Multi-sig: multiple sigs + script - 'scriptPubKey_size': 34, # OP_0 - 'on_chain_distinguishable': True, - 'typical_input_size': 34 + 180, - 'description': 'SegWit v0 Multi-sig: Pay to Witness Script Hash (2-of-3)' - }, - { - 'type': 'P2TR-key', - 'witness_size': 65, # Single 64-byte signature + 1-byte flag - 'scriptPubKey_size': 34, # OP_1 <32-byte key> - 'on_chain_distinguishable': False, - 'typical_input_size': 34 + 65, - 'description': 'Taproot key-path spend (most common case, 2021-present)' - }, - { - 'type': 'P2TR-script', - 'witness_size': 130, # Signature + script + merkle proof - 'scriptPubKey_size': 34, # OP_1 <32-byte key> - IDENTICAL to key-path! - 'on_chain_distinguishable': False, - 'typical_input_size': 34 + 130, - 'description': 'Taproot script-path spend (fallback if key path unavailable)' - } -] - - -# Block data for adoption tracking -SAMPLE_BLOCKS = [ - { - 'height': 700000, - 'date': '2021-11-13', - 'p2pkh_count': 150, - 'p2sh_count': 100, - 'p2wpkh_count': 250, - 'p2wsh_count': 30, - 'p2tr_count': 5, - 'total_outputs': 535 - }, - { - 'height': 702000, - 'date': '2021-12-05', - 'p2pkh_count': 140, - 'p2sh_count': 95, - 'p2wpkh_count': 280, - 'p2wsh_count': 35, - 'p2tr_count': 50, - 'total_outputs': 600 - }, - { - 'height': 704000, - 'date': '2021-12-27', - 'p2pkh_count': 130, - 'p2sh_count': 90, - 'p2wpkh_count': 310, - 'p2wsh_count': 40, - 'p2tr_count': 130, - 'total_outputs': 700 - }, - { - 'height': 750000, - 'date': '2023-04-04', - 'p2pkh_count': 80, - 'p2sh_count': 70, - 'p2wpkh_count': 400, - 'p2wsh_count': 80, - 'p2tr_count': 470, - 'total_outputs': 1100 - }, - { - 'height': 800000, - 'date': '2024-05-15', - 'p2pkh_count': 50, - 'p2sh_count': 40, - 'p2wpkh_count': 380, - 'p2wsh_count': 100, - 'p2tr_count': 930, - 'total_outputs': 1500 - } -] - - -# ============================================================================ -# Exercise Functions - Implement These! -# ============================================================================ - -def compare_output_sizes(tx_types: List[Dict]) -> Dict[str, int]: - """ - Compare scriptPubKey sizes across different transaction types. - - This reveals the privacy issue: Legacy formats have different output sizes. - An observer can identify address types just by looking at output length! - - Args: - tx_types: List of transaction type dicts - - Returns: - Dict mapping tx_type name -> scriptPubKey_size - - Privacy Insight: - - P2PKH outputs are 25 bytes: instantly recognizable - - P2SH outputs are 20 bytes: known script hash size - - Segwit v0 outputs are 22 bytes: identifiable - - Taproot outputs are 34 bytes: UNIFORM regardless of spend type - (key-path spending looks identical to script-path!) - - This is crucial: with Taproot, observer cannot distinguish common cases. - """ - # TODO: Implement this function - # Hint: Extract scriptPubKey_size from each tx_type, return as dict - pass - - -def compare_spend_sizes(tx_types: List[Dict]) -> Dict[str, int]: - """ - Compare witness (input) sizes across transaction types. - - Witness data is what's needed to spend coins (signatures). Smaller witnesses - mean cheaper transactions. This incentivizes privacy-preserving spends. - - Args: - tx_types: List of transaction type dicts - - Returns: - Dict mapping tx_type name -> total_input_size - - Privacy Insight: - - P2PKH inputs are ~148 bytes (no witness separation) - - Segwit witnesses are ~108 bytes for P2WPKH (30% smaller) - - Taproot key-path is only 65 bytes (50% smaller than P2WPKH!) - - This is because signatures are smaller and more efficient - - Honest users are incentivized to use the private option - - Economic incentive for privacy: Taproot key-path is the CHEAPEST option, - so privacy-conscious users naturally choose it for cost reasons alone. - This creates a large anonymity set of "normal" transactions. - """ - # TODO: Implement this function - # Hint: Extract typical_input_size from each tx_type, return as dict - pass - - -def analyze_privacy_improvement(tx_types: List[Dict]) -> Dict[str, List[str]]: - """ - Identify which transaction types are distinguishable on-chain. - - A transaction type is distinguishable if an observer can identify it - just from public blockchain data (output size, witness structure, etc). - - Distinguishability is bad for privacy: attacker knows your transaction type, - limiting anonymity and enabling targeted surveillance. - - Args: - tx_types: List of transaction type dicts - - Returns: - Dict with keys: - - 'distinguishable': list of types that can be identified on-chain - - 'indistinguishable': list of types that look identical on-chain - - 'taproot_advantage': explanation of why Taproot improves privacy - - Privacy Implication: - - With old types: attacker knows instantly "this is P2PKH" or "this is P2WPKH" - They know your spending pattern, address reuse habits, change patterns - - With Taproot: attacker cannot distinguish key-path from script-path - (or from other advanced spending conditions that might exist) - """ - # TODO: Implement this function - # Hint: Check on_chain_distinguishable for each type - pass - - -def calculate_anonymity_set(tx_type: str, network_usage: Dict[str, float]) -> float: - """ - Calculate the anonymity set for a given transaction type. - - The anonymity set is: "How many other users have the same transaction type?" - Larger anonymity set = better privacy (harder to single you out). - - If 99% of network uses P2WPKH and you use P2WSH, you stand out. - If 50% uses P2TR and you use P2TR, you blend in with millions. - - Args: - tx_type: The type to calculate anonymity for (e.g., 'P2TR-key') - network_usage: Dict mapping tx_type -> percentage (0-100) of network - - Returns: - float representing the fraction of the network using this type - - Privacy Insight: - - Anonymity set = % of network using the same type - - P2TR adoption growing rapidly (as of 2024, ~60%+ of new outputs) - - Users choosing P2TR benefit from large anonymity set - - Users still using P2PKH are alone in a pool of 0.5% - - Example: - If network_usage = {'P2TR-key': 60, 'P2WPKH': 30, 'P2WSH': 5, ...} - calculate_anonymity_set('P2TR-key', network_usage) -> 60.0 - """ - # TODO: Implement this function - # Hint: Return the percentage value from network_usage dict for given tx_type - pass - - -def simulate_cisa_savings(num_inputs: int) -> Dict[str, float]: - """ - Calculate fee savings from Cross-Input Signature Aggregation (CISA). - - CISA is a Taproot feature (not yet deployed) that allows multiple signatures - to be aggregated into one. In a CoinJoin with 10 people, you could have - 1 signature instead of 10 - massive savings! - - Signature size assumptions: - - Taproot: 65 bytes per signature - - With CISA: 1 signature covers all inputs (only 65 bytes total!) - - Args: - num_inputs: Number of CoinJoin participants (inputs) - - Returns: - Dict with: - - 'without_cisa_bytes': witness bytes needed without CISA - - 'with_cisa_bytes': witness bytes with CISA - - 'bytes_saved': absolute savings - - 'percent_saved': % reduction in transaction size - - 'fee_savings_at_50sat_per_byte': cost savings in sats - - Fee calculation (simplified): - - Transaction cost = size_in_bytes × fee_rate (sats/byte) - - Typical high-priority fee: ~50 sats/byte - - If CISA saves 500 bytes, that's 500 × 50 = 25,000 sats saved! - - Network Effect: - - CoinJoin becomes radically cheaper with CISA - - More people join for privacy (CoinJoins become larger) - - Privacy becomes accessible to everyone (not just wealthy users) - - Network privacy improves as more people participate - """ - # TODO: Implement this function - # Hint: Calculate based on num_inputs * 65 bytes per signature - pass - - -def taproot_adoption_analysis(blocks: List[Dict]) -> Dict: - """ - Analyze Taproot adoption trends over time. - - This function tracks how many outputs in each block are P2TR vs other types. - High adoption = large anonymity set = better privacy for P2TR users. - - Args: - blocks: List of block data dicts with output counts by type - - Returns: - Dict with: - - 'first_block': block height of first sample - - 'last_block': block height of last sample - - 'initial_adoption_percent': P2TR % in first block - - 'final_adoption_percent': P2TR % in last block - - 'growth_rate_percent_per_block': average growth per 2000 blocks - - 'adoption_by_block': dict mapping block height -> adoption % - - 'anonymity_trajectory': string describing trend - - Privacy Timeline: - - Early (2021-2022): Low P2TR adoption (<5%), small anonymity set - - Growth (2022-2023): Rapid adoption as wallets add support (~30%) - - Mainstream (2023-2024): Majority of new outputs are P2TR (~60%+) - - Future: P2TR dominance, excellent privacy from size alone - - Note: High adoption doesn't mean perfect privacy, but it's a key factor - in making privacy the default for economically rational users. - """ - # TODO: Implement this function - # Hint: Calculate P2TR percentage for each block, track growth trend - pass - - -# ============================================================================ -# Main Analysis Block -# ============================================================================ - -def main(): - """ - Run comprehensive Taproot privacy analysis. - - This demonstrates all concepts by comparing transaction types, - calculating anonymity, and projecting CISA benefits. - """ - - print("=" * 80) - print("TAPROOT PRIVACY ANALYSIS LAB") - print("=" * 80) - print() - - # ======================================================================== - # Part 1: Output Size Comparison - # ======================================================================== - print("PART 1: OUTPUT SIZE COMPARISON") - print("-" * 80) - print("Privacy Issue: If outputs have different sizes, observer identifies type") - print() - - output_sizes = compare_output_sizes(SAMPLE_TX_TYPES) - if output_sizes: - for tx_type, size in sorted(output_sizes.items(), key=lambda x: x[1]): - print(f" {tx_type:15s}: {size:2d} bytes", end="") - if size == 34: - print(" <- Taproot (uniform)") - else: - print(f" <- Legacy (recognizable: {size} byte fingerprint)") - print() - - # ======================================================================== - # Part 2: Input/Witness Size Comparison - # ======================================================================== - print("PART 2: INPUT/WITNESS SIZE COMPARISON") - print("-" * 80) - print("Privacy Insight: Smaller witnesses incentivize privacy-preserving spends") - print("Economic incentive: Key-path is cheapest, so privacy becomes default") - print() - - spend_sizes = compare_spend_sizes(SAMPLE_TX_TYPES) - if spend_sizes: - # Sort by size to show efficiency improvement - sorted_sizes = sorted(spend_sizes.items(), key=lambda x: x[1]) - min_size = sorted_sizes[0][1] - for tx_type, size in sorted_sizes: - percent_overhead = ((size - min_size) / min_size) * 100 - print(f" {tx_type:15s}: {size:3d} bytes", end="") - if percent_overhead == 0: - print(" <- CHEAPEST (key-path Taproot)") - else: - print(f" ({percent_overhead:+5.0f}% overhead)") - print() - - # ======================================================================== - # Part 3: Distinguishability Analysis - # ======================================================================== - print("PART 3: ON-CHAIN DISTINGUISHABILITY ANALYSIS") - print("-" * 80) - print("Privacy Risk: If type is identifiable, attacker knows your structure") - print() - - privacy_analysis = analyze_privacy_improvement(SAMPLE_TX_TYPES) - if privacy_analysis: - print("Distinguishable types (attacker knows your type instantly):") - if privacy_analysis.get('distinguishable'): - for tx_type in privacy_analysis['distinguishable']: - print(f" - {tx_type}") - print() - print("Indistinguishable types (attacker cannot tell them apart):") - if privacy_analysis.get('indistinguishable'): - for tx_type in privacy_analysis['indistinguishable']: - print(f" - {tx_type}") - print() - if privacy_analysis.get('taproot_advantage'): - print(f"Taproot Advantage: {privacy_analysis['taproot_advantage']}") - print() - - # ======================================================================== - # Part 4: Anonymity Set Analysis - # ======================================================================== - print("PART 4: ANONYMITY SET ANALYSIS") - print("-" * 80) - print("Larger anonymity set = harder for attacker to target you") - print() - - # Simulate network usage (actual network data from mid-2024) - network_usage = { - 'P2PKH': 3, - 'P2SH': 2, - 'P2WPKH': 25, - 'P2WSH': 5, - 'P2TR-key': 60, - 'P2TR-script': 5 - } - - print("Network adoption (% of new outputs):") - for tx_type, percent in sorted(network_usage.items(), key=lambda x: -x[1]): - anonymity = calculate_anonymity_set(tx_type, network_usage) - if anonymity: - print(f" {tx_type:15s}: {anonymity:5.1f}% of network", end="") - if anonymity >= 50: - print(" <- Large anonymity set (excellent privacy)") - elif anonymity >= 20: - print(" <- Medium anonymity set (good privacy)") - else: - print(" <- Small anonymity set (poor privacy)") - print() - - # ======================================================================== - # Part 5: CISA Savings Simulation - # ======================================================================== - print("PART 5: CROSS-INPUT SIGNATURE AGGREGATION (CISA) SAVINGS") - print("-" * 80) - print("CISA: Single signature covers all inputs (potential future upgrade)") - print("Use case: CoinJoin transactions with many participants") - print() - - coinjoin_sizes = [3, 5, 10, 50, 100] - for size in coinjoin_sizes: - savings = simulate_cisa_savings(size) - if savings: - print(f"CoinJoin with {size:3d} participants:") - print(f" Without CISA: {savings['without_cisa_bytes']:5.0f} bytes witness") - print(f" With CISA: {savings['with_cisa_bytes']:5.0f} bytes witness") - print(f" Savings: {savings['bytes_saved']:5.0f} bytes ({savings['percent_saved']:5.1f}%)") - print(f" Fee savings: {savings['fee_savings_at_50sat_per_byte']:7.0f} sats at 50sat/byte") - print() - - # ======================================================================== - # Part 6: Adoption Trajectory - # ======================================================================== - print("PART 6: TAPROOT ADOPTION TRAJECTORY") - print("-" * 80) - print("Privacy improves as more users adopt Taproot") - print() - - adoption_data = taproot_adoption_analysis(SAMPLE_BLOCKS) - if adoption_data: - print(f"Analysis period: Block {adoption_data['first_block']} to {adoption_data['last_block']}") - print(f"Initial adoption: {adoption_data['initial_adoption_percent']:.1f}% P2TR") - print(f"Final adoption: {adoption_data['final_adoption_percent']:.1f}% P2TR") - print(f"Growth rate: {adoption_data['growth_rate_percent_per_block']:.3f}% per 2000 blocks") - print(f"Trajectory: {adoption_data['anonymity_trajectory']}") - print() - - # Adoption timeline - if adoption_data.get('adoption_by_block'): - print("Adoption by block:") - for block_height in sorted(adoption_data['adoption_by_block'].keys()): - adoption_pct = adoption_data['adoption_by_block'][block_height] - bar_length = int(adoption_pct / 5) - bar = "█" * bar_length - print(f" Block {block_height}: {adoption_pct:6.1f}% {bar}") - print() - - # ======================================================================== - # Summary - # ======================================================================== - print("=" * 80) - print("PRIVACY IMPLICATIONS SUMMARY") - print("=" * 80) - print() - print("Taproot (SegWit v1) improves Bitcoin privacy through:") - print("1. UNIFORM OUTPUTS: All P2TR outputs look identical (32 bytes)") - print(" - Prevents attacker from identifying transaction type") - print(" - Makes script-path spends indistinguishable from key-path") - print() - print("2. EFFICIENT KEY-PATH: Smallest signature (65 bytes vs 71-73 bytes)") - print(" - Economic incentive: key-path is cheapest") - print(" - Privacy becomes the default choice (not premium feature)") - print() - print("3. LARGE ANONYMITY SET: Majority of new outputs are P2TR") - print(" - As adoption increases, P2TR users blend in naturally") - print(" - You can't identify users by transaction type alone") - print() - print("4. CISA POTENTIAL: Future upgrade for even better CoinJoin") - print(" - Massive fee savings for privacy transactions") - print(" - Makes CoinJoin accessible to regular users") - print() - - -if __name__ == '__main__': - main() diff --git a/privacy-track/phase-5-advanced/README.md b/privacy-track/phase-5-advanced/README.md deleted file mode 100644 index bdbe20f..0000000 --- a/privacy-track/phase-5-advanced/README.md +++ /dev/null @@ -1,714 +0,0 @@ -# Phase 5: Advanced Techniques in Bitcoin Privacy -## Sessions 17-20 - -This phase moves beyond basics into production-grade privacy tools. You'll master CoinJoin, learn emerging techniques like CoinSwap, explore federated eCash, and build real-world Lightning privacy awareness. By the end, you'll understand every major privacy technique and be able to choose the right tool for the right scenario. - ---- - -## Session 17: CoinJoin - WabiSabi, JoinMarket, Collaborative Transactions - -### Real-World Scenario - -You bought 0.5 BTC on Coinbase with full KYC. Your ID is linked to that Bitcoin address. Now you want to buy coffee at a merchant who doesn't trust chain analysis. If you spend your KYC Bitcoin directly, Coinbase will know you bought coffee. Their compliance team could even flag the merchant. - -A CoinJoin transaction pools your coins with 50 other participants. The transaction has 51 identical 0.01 BTC outputs. Nobody - not Coinbase, not the chain analyst, not the merchant - can prove which output is "yours." The link between your identity and your future spending is broken. - -**This is how real people reclaim their privacy.** - -### Learning Objectives - -- Understand the WabiSabi protocol and how it improves on Wasabi v1.0 -- Analyze CoinJoin transactions to quantify anonymity (Anonset) -- Execute a practical CoinJoin in Wasabi Wallet -- Recognize the strengths and limitations of CoinJoin as a privacy tool -- Configure JoinMarket for yield farming while maintaining privacy - -### Privacy Tools You'll Use - -- **Wasabi Wallet** (WabiSabi CoinJoin coordinator) -- **Sparrow Wallet** (UTXO management, coin control, CoinJoin prep) -- **JoinMarket** (market-based mixing, liquidity provider) -- **OXT.me** (on-chain CoinJoin analysis) -- **Stratum mining pool analyzer** (to understand output clustering) - -### Detailed Hands-On Exercise - -#### Part 1: Execute a Real CoinJoin in Wasabi (60 minutes) - -1. **Install & fund Wasabi Wallet** - - Download Wasabi Wallet from wasabiwallet.io - - Verify GPG signature (learn why this matters for privacy software) - - Create a new wallet with a 12-word seed - - Fund it with 0.01 BTC from a public testnet faucet (we'll use testnet for safety) - - Wait for 6 confirmations - -2. **Prepare for CoinJoin** - - In Wasabi's Coins tab, label your UTXO: "KYC input from exchange" - - Enable coin control (Settings > Mix > Advanced) - - Review the current Wasabi coordinator fee (typically 0.3%) - - Check the current waiting queue (how many transactions ahead of you?) - -3. **Initiate the CoinJoin** - - Select your coin in the Coins tab - - Click "Enqueue" - - Wasabi begins searching for mixing partners - - Watch the transaction build in real-time (or come back in 10-60 minutes) - - **Key learning:** The mixing output is derived using Wasabi's output selection algorithm. This is intentional - round outputs (0.01, 0.03, 0.1) make it harder to fingerprint which output is "yours" - -4. **Analyze the CoinJoin Output** - - Once mixed, open the transaction on OXT.me - - Count the total number of outputs - - Identify the equal-denomination outputs (the anonymity set) - - Calculate your anonset: How many outputs could yours be? - - Check the change output (Wasabi sends change to a new address for privacy) - - **Question to answer:** If outputs are 0.01 BTC and your input was 0.5 BTC, what does that tell a chain analyst? - -5. **Document the Round** - - Wasabi tracks your round in the transaction history - - Screenshot: the round confirmation - - Screenshot: the anonset achieved - - Write a 200-word summary: "What privacy did this transaction give me?" - -#### Part 2: Analyze a Real Mainnet CoinJoin (90 minutes) - -1. **Pick a live Wasabi CoinJoin from mainnet** - - Visit OXT.me > CoinJoin Coordinators - - Find a recent Wasabi transaction (look for WabiSabi in the protocol field) - - Example: Search txid on OXT.me (an actual Wasabi transaction) - -2. **Trace the transaction** - - Identify all inputs (who's mixing?) - - Count outputs and find the equal-denomination cluster - - Use OXT.me's "Merge Fields" detector to identify change outputs - - **Challenge:** Can you identify which output might be the change? (Hint: Often slightly smaller or sent immediately to another address) - -3. **Calculate Anonset** - - Count equal-denomination outputs: `N` - - This is your **forward anonset** - when you spend, how many outputs could yours be? - - Now follow one of those outputs: if it was CoinJoined again, multiply the anonsets - - This is your **heuristic anonset** - accounting for multiple rounds - -4. **Identify Clustering Attacks** - - Which outputs were spent within 24 hours? (suggests urgency, might leak timing info) - - Which outputs were sent to the same merchant (identifies the requester) - - Did outputs go to exchange deposit addresses? (leaked identity) - - Write down: "This transaction leaked privacy because..." - -#### Part 3: Set Up JoinMarket as a Yield Farmer (120 minutes) - -1. **Install JoinMarket** - - Clone the repo: `git clone https://github.com/joinmarket-org/joinmarket-clientserver.git` - - Follow installation for your OS - - Initialize the wallet: `python3 wallet-tool.py generate` - - Save your seed phrase securely - -2. **Fund and Configure** - - Fund with 0.05 BTC (small amount for learning) - - Wait for 6 confirmations - - Create a new Yield Generator mode config - - Set conservative fee: 0.0005 BTC per join - - Set mix depth to 5 (changes address for each join) - -3. **Start Yield Generating** - - Launch the yield generator: `python3 yield-generator.py` - - Monitor the logs as other users pay you to mix with them - - **Learn:** You're being paid to provide liquidity. Earn sats while mixing others' coins - - Run for at least 1 hour, monitor earnings - -4. **Analyze Your Yields** - - Export transaction history - - Calculate: "How much sats did I earn? How many joins did I participate in?" - - Calculate: "If I earn 10,000 sats/month, what's my effective hourly rate?" - - **Trade-off analysis:** You earned privacy (your coins mixed) AND sats. But you locked capital. Was it worth it? - -### Real-World Exercise: Read the WabiSabi Paper - -1. Read the core sections of "WabiSabi: Centrally Coordinated CoinJoin with Variable Amounts" (arXiv:2202.12951) -2. Focus on: Protocol overview, Zero-Knowledge Proofs section, Input/output alignment -3. Answer: "Why does WabiSabi allow variable-amount inputs, unlike Wasabi v1.0?" -4. Answer: "What prevents the coordinator from stealing funds?" - -### Open a PR This Week - -**Choose one:** - -1. **Wasabi Wallet Documentation** - - Repo: `zkSNACKs/WalletWasabi` - - Contribute: Add a "Privacy Tutorial: Your First CoinJoin" section to the docs - - Target: Real-world guide for non-technical users - -2. **OXT.me Analysis Tool** - - Repo: `graymauser/opschain-ox` or similar chain analysis projects - - Contribute: Add an "Anonset Calculator" tool that auto-computes forward/heuristic anonset - - Target: Python script, or JavaScript widget - -3. **JoinMarket Configuration Guide** - - Repo: `joinmarket-org/joinmarket-clientserver` - - Contribute: Add privacy-focused configuration examples (conservative fees, optimal mix depth) - - Target: Wiki update with 5+ config scenarios for different user types - -### Daily Life Privacy Tip - -**If you have KYC Bitcoin, your first spending action should be a CoinJoin.** - -Here's the minimal version: -1. Create a new Wasabi Wallet (or Sparrow with CoinJoin support) -2. Send your KYC Bitcoin to a fresh Wasabi address -3. Enqueue for CoinJoin (Wasabi handles the rest) -4. Once mixed, spend the output (the merchant has no link to your KYC identity) - -Time investment: ~30 minutes. Cost: ~0.3% fee. Privacy gain: You broke the identity-to-spending link. - -### Reading List - -1. **WabiSabi Protocol** (Academic) - - "WabiSabi: Centrally Coordinated CoinJoin with Variable Amounts" - - arXiv: https://arxiv.org/pdf/2202.12951 - - Focus: Protocol mechanics, ZK proofs - -2. **Wasabi Wallet Docs** (Practical) - - https://docs.wasabiwallet.io/using-wasabi/CoinJoin.html - - Real-world configuration and walkthrough - -3. **JoinMarket Docs** (Advanced) - - https://joinmarket.readthedocs.io/ - - Yield generation, market dynamics - -4. **OXT.me Analysis Tool** (Data) - - https://oxt.me/ - - Live CoinJoin observation, on-chain metrics - ---- - -## Session 18: CoinSwap & Atomic Swaps for Privacy - -### Real-World Scenario - -You're a merchant who receives customer payments on-chain. But you don't want anyone knowing how much Bitcoin you have. CoinJoin shows a clear pattern: 51 identical outputs, obviously a mixing round. - -CoinSwap is different. You swap coins with another Bitcoin user, 1-to-1, in a series of smart contract-like transactions. From the outside, it looks like a normal Bitcoin transaction. No obvious pattern. No anonymity set. The blockchain observer has no idea what happened. - -**This is stealth privacy - it looks like nothing at all.** - -### Learning Objectives - -- Understand the CoinSwap protocol and how it differs from CoinJoin -- Learn why CoinSwap transactions are indistinguishable from normal payments -- Trace a CoinSwap step-by-step (theory) -- Understand Atomic Swaps and their privacy implications -- Recognize when CoinSwap or Atomic Swaps are the right tool - -### Privacy Tools You'll Use - -- **Teleport Transactions** (proposed CoinSwap implementation) -- **citadel-tech/coinswap** (experimental reference implementation) -- **atomic-swap-tools** (for cross-chain learning) -- **Paper and diagram tools** (to understand the protocol without executing) - -### Detailed Hands-On Exercise - -#### Part 1: Read & Understand the CoinSwap Specification (120 minutes) - -1. **Read the CoinSwap BIP draft** - - Original proposal: CoinSwap concept by Chris Belcher - - Current repo: `citadel-tech/coinswap` - - Read: The protocol overview and multi-transaction flow - - **Key sections:** Why CoinSwap is better than CoinJoin for privacy, the maker/taker model, Hash time-locked contracts - -2. **Draw a CoinSwap on Paper** - - You and Alice want to swap coins - - You have 1 BTC at address A - - Alice has 1 BTC at address B - - Draw the transaction sequence (setup, broadcast, settlement phases) - - **Question:** Can a chain analyst tell that you and Alice swapped? What would they see? - -3. **Compare: CoinJoin vs CoinSwap** - - CoinJoin: 51 inputs, 51 equal outputs (obvious pattern) - - CoinSwap: 1 input, 1 output each (looks normal) - - Create a comparison table covering blockchain pattern, anonymity set, speed, coordination, capital required - - **Conclusion:** When would YOU choose CoinSwap? - -#### Part 2: Trace a Simulated CoinSwap (90 minutes) - -1. **Build a theoretical CoinSwap scenario** - - You: 0.5 BTC at address `1Alice...` (KYC from exchange) - - Maker: 0.5 BTC at address `1Maker...` (unknown identity) - - Create 4 addresses for yourself: setup, swap, redeem, final - - Create 4 addresses for Maker - -2. **Trace the transaction flow** - - Tx1: You send 0.5 BTC to setup (locked contract, Maker can't spend) - - Tx2: Maker sends 0.5 BTC to setup (locked contract, you can't spend) - - Tx3: You spend Maker's output to redeem - - Tx4: Maker spends your output to final - - Result: You now control Maker's output, Maker controls your output - - **Privacy consequence:** The address has no obvious connection to your redeemed output - -3. **Attempt a clustering attack** - - You're a chain analyst looking at the addresses - - Can you connect them to one entity? - - Check: Does any output get spent to a KYC exchange deposit? - - Check: Do the addresses re-spend within 1 hour? - - **Finding:** Even on paper, CoinSwap is much harder to trace than CoinJoin - -#### Part 3: Understand Atomic Swaps (60 minutes) - -1. **Read: How Atomic Swaps Work (Cross-Chain)** - - Atomic Swap = CoinSwap across blockchains - - Example: You swap Bitcoin for Monero 1-to-1 - - The same hash-timelock mechanism works - -2. **Privacy Implications** - - Atomic Swaps to Monero break the Bitcoin-to-identity link completely - - Once you're in Monero, privacy is automatic - - Downside: Requires trusted exchange infrastructure - -3. **Security Analysis** - - If an atomic swap partially completes, what happens? - - Answer: Hash time-locked contracts force both parties to complete or both refund - - This is the security property that makes CoinSwap work - -### Real-World Exercise: Propose a CoinSwap Market - -Design (on paper) a peer-to-peer CoinSwap market: -1. How do makers advertise their swap offers? -2. How do takers find matches? -3. What happens if one party doesn't complete? -4. How is pricing set? -5. Can it be done without a centralized coordinator? - -Write a 500-word proposal. - -### Open a PR This Week - -**Choose one:** - -1. **citadel-tech/coinswap** - - Contribute: Improve documentation or add a "CoinSwap for Bitcoin Privacy" guide - - Target: Make the protocol understandable to developers - -2. **Bitcoin Privacy Research** - - Contribute: Write a comparative analysis: "CoinSwap vs CoinJoin: When to Use Each" - - Target: Published analysis document - -3. **Atomic Swap Reference Implementation** - - Contribute to: `comit-network/comit-rs` or similar atomic swap projects - - Target: Documentation or example scenarios - -### Daily Life Privacy Tip - -**If you want invisible privacy, CoinSwap is better than CoinJoin - but you need a swap partner.** - -Reality check: CoinSwap is still experimental. For today: -1. Use CoinJoin (Wasabi) for your routine privacy -2. Watch CoinSwap development (citadel-tech/coinswap on GitHub) -3. If a trusted CoinSwap service launches, try it for a second privacy layer - -### Reading List - -1. **CoinSwap Protocol** - - Chris Belcher's original proposal: https://bitcointalk.org/index.php?topic=321228.0 - - citadel-tech coinswap repo: https://github.com/citadeltech/coinswap - -2. **Scriptless Scripts & Hash Time-Locked Contracts** - - "Scriptless Scripts via Discrete Log Contracts" - Poelstra - - https://github.com/ElementsProject/scriptless-scripts - -3. **Atomic Swaps** - - "Atomic Swaps: Cross-Chain Transactions" - Tier Nolan, 2013 - ---- - -## Session 19: eCash - Fedimint & Cashu for Privacy-Preserving Communities - -### Real-World Scenario - -Bitcoin House Bali is a community hub where 50 people work and spend Bitcoin daily. But every transaction is public on-chain. They don't want the Indonesian government seeing exactly who paid whom for food, coworking, or events. - -Solution: The community runs a Fedimint federation. Members deposit Bitcoin once. They receive eCash tokens (blind coins). When two members transact, they swap eCash tokens. The Bitcoin stays locked on-chain. The Fedimint federation cannot see who paid whom - the blind signatures ensure mathematical privacy. - -**This is institutional privacy: Bitcoin goes in, perfect privacy comes out.** - -### Learning Objectives - -- Understand Fedimint architecture: guardians, federations, eCash protocols -- Learn blind signatures and why they provide mathematical privacy -- Execute a Cashu mint locally and issue/redeem tokens -- Understand the trade-offs: trust vs. privacy -- Design a real-world eCash federation for a community - -### Privacy Tools You'll Use - -- **Fedimint** (production federation framework) -- **Cashu** (lightweight eCash protocol, in development) -- **Fedi app** (mobile eCash wallet) -- **Blind Signature Libraries** (cryptographic primitives) -- **Docker** (to run a local test federation) - -### Detailed Hands-On Exercise - -#### Part 1: Understand Blind Signatures (90 minutes) - -1. **Read the theory** - - Blind Signature concept: Created by David Chaum (1983) - - How it works: User blinds a message, server signs it blindly, user unblinds - - Privacy property: Server learns nothing about the message it signed - - Resource: Read "Blind Signatures for Untraceable Payments" - Chaum, 1983 - -2. **Implement a toy blind signature scheme** - - Use Python or JavaScript - - Create a simple example: RSA blind signatures - - Code outline showing message blinding, blind signing, and unblinding - - **Key insight:** The server signed something, but couldn't see what it signed - -3. **Answer the privacy question** - - If I deposit 1 BTC and receive blind-signed eCash tokens, can the server track my spending? - - Answer: No. The server can verify the signature is valid, but doesn't know which token is yours - - Compare: Bitcoin (all transactions public) vs eCash (all transactions private to federation) - -#### Part 2: Deploy a Local Cashu Mint (120 minutes) - -1. **Install Cashu (nutshell implementation)** - - Clone: `git clone https://github.com/cashubtc/nutshell.git` - - Install dependencies: `pip install -r requirements.txt` - - Start a mint server: `python -m nutshell.mint` - -2. **Create eCash Tokens** - - Use a Cashu client to request tokens - - Request: 0.01 BTC in eCash - - The mint creates blind signatures for your tokens - - You receive a token string - -3. **Transfer Tokens (Peer-to-Peer)** - - You and a partner use the same Cashu mint - - You want to send them 0.005 BTC (eCash) - - You: Send the token string to them - - Partner: Redeem the token at the mint - - Mint: Verifies and marks the token as spent - - **Key privacy win:** No blockchain record. The mint can't tell who transferred to whom - -4. **Redeem eCash Back to Bitcoin** - - Redeem 0.005 BTC of eCash at the mint - - The mint sends Bitcoin to your address - - Check: The Bitcoin transaction is on-chain, but earlier privacy transactions were invisible - -5. **Analyze the Privacy Leakage** - - When you redeem eCash to Bitcoin, you provide an on-chain address - - This reveals: "Someone redeemed X BTC" - - But earlier transactions are private - - **Design question:** How would you minimize this leakage? - -#### Part 3: Design a Fedimint Federation for Your Community (120 minutes) - -1. **Fedimint Architecture Overview** - - Guardians: N trusted people - - Consensus: Guardians must agree on Bitcoin deposits (3-of-5 multisig) - - eCash: Users deposit Bitcoin, receive eCash tokens - - Privacy: No guardian individually sees all transactions - -2. **Design Your Federation** - - Community: Choose 20-50 people - - Guardians: Select 5-7 trusted leaders - - Multisig: 3-of-5 consensus model - - Deposit, spending, and redemption processes - -3. **Governance Document** - - Create a "Fedimint Governance Charter" for your community - - Cover: Guardian selection, succession plan, multisig threshold, transaction limits, dispute resolution, mint expiration - - Write 500-750 words - -4. **Trust vs. Privacy Trade-off Analysis** - - Create a comparison table: Bitcoin On-Chain vs Cashu vs Fedimint - - Cover: Privacy, trust required, redemption risk, ease of use - - Conclusion: Write 200 words on when each is appropriate - -### Real-World Exercise: Interview a Fedimint Guardian - -If possible, contact someone running a Fedimint federation. Ask: -1. How many users do you have? -2. What's the total Bitcoin in the federation? -3. How do you prevent double-spending of eCash? -4. What's the redemption process like? -5. Have you had any privacy breaches? - -Synthesize their answers into a case study (500 words). - -### Open a PR This Week - -**Choose one:** - -1. **Fedimint Documentation** - - Repo: `fedimint/fedimint` - - Contribute: Add "Setting Up Your First Federation: A Community Guide" - - Target: Tutorial for non-technical communities - -2. **Cashu Protocol Improvements** - - Repo: `cashubtc/nutshell` - - Contribute: Documentation, test cases, or privacy analysis - - Target: Help stabilize the Cashu protocol - -3. **eCash Privacy Analysis** - - Publish a blog post analyzing: "Fedimint Privacy Properties" - - Target: Compare to CoinJoin, CoinSwap, and native Bitcoin - -### Daily Life Privacy Tip - -**If your community is Bitcoin-native, run a Fedimint federation.** - -Simple path: -1. Identify 5-7 trusted people in your community -2. Run Fedimint together -3. Members deposit Bitcoin, receive eCash -4. All internal transactions are invisible -5. Bitcoin stays locked on-chain, fully secured - -Cost: Time to set up + server hosting -Privacy gain: Perfect privacy for community transactions - -### Reading List - -1. **Blind Signatures & eCash** - - "Blind Signatures for Untraceable Payments" - David Chaum - - https://chaum.com/blind-signatures/ - -2. **Fedimint Protocol** - - https://github.com/fedimint/fedimint - - https://fedimint.org/ - -3. **Cashu Protocol** - - https://github.com/cashubtc/cashu - - https://github.com/cashubtc/cashu/blob/main/docs/00-overview.md - -4. **Fedi App (Mobile eCash Wallet)** - - https://fedi.xyz/ - ---- - -## Session 20: Lightning Network Privacy - BOLT12, Blinded Paths & Channel Probing - -### Real-World Scenario - -You're a merchant using Lightning Network for instant payments. Every time a customer pays you, your node's public key appears in the transaction. Analyzing many Lightning payments, a chain analyst could map your payment graph and understand your transaction volume, which addresses you receive from, and even physical location. - -BOLT12 (Offers with Blinded Paths) solves this: Instead of giving your node ID, you share a "blinded offer." The payer can't see your node's actual identity. They route through blinded paths. Result: Your node remains unknown. - -**This is Lightning privacy: Instant payments, invisible sender and receiver.** - -### Learning Objectives - -- Understand BOLT12 offers and why they improve on BOLT11 invoices -- Learn blinded paths and how they hide node identities -- Execute a BOLT12 offer with blinded paths -- Analyze what information a payer can and cannot learn -- Recognize Lightning privacy risks (channel probing, flow analysis) -- Implement channel probing defenses - -### Privacy Tools You'll Use - -- **Core Lightning** (BOLT12 native support) -- **LDK** (Lightning Development Kit, BOLT12 in development) -- **Phoenix Wallet** (blinded paths support) -- **Eclair** (potential BOLT12 support) -- **Lightning Network Analyzer Tools** (to understand graph structure) - -### Detailed Hands-On Exercise - -#### Part 1: Understand BOLT12 vs BOLT11 (90 minutes) - -1. **Review BOLT11 (Classic Invoices)** - - Format: Static invoice with node ID, amount, expiry - - Example: `lnbc100n1p0...` contains your node's identity - - Privacy problem: Every invoice reveals your node ID - - Reuse problem: Same invoice can be paid multiple times - -2. **Learn BOLT12 (Offers with Privacy)** - - Format: Non-specific offer with blinded path - - Example: Offer encodes "I accept payments via blinded path X" - - Privacy property: Payer doesn't see your actual node ID - - Flexibility: Offers can be static (used many times) or one-time - -3. **Compare in a Table** - - Feature comparison: Node ID exposed, Multiple payments, Reusability, Payer knowledge, Implementation status - - Clear privacy advantages of BOLT12 - -4. **Read the BOLT12 Specification** - - https://github.com/lightning/bolts/blob/master/12-offer.md - - Focus: Offer format, blinded path construction, privacy properties - - Answer: Why can't a payer learn your node ID even if they want to? - -#### Part 2: Create and Use a BOLT12 Offer (120 minutes) - -**Note:** This exercise requires Core Lightning running. Use testnet or a local Lightning network simulator. - -1. **Set Up Core Lightning** - - Install: https://github.com/ElementsProject/lightning - - Start a node: `lightningd --testnet --dev-fast-gossip` - - Verify: `lightning-cli getinfo` shows your node ID - -2. **Create a BOLT12 Offer** - - Command: `lightning-cli offer ` - - Example: `lightning-cli offer 1000000msat "Coffee"` - - Output: A `bolt12://` string with a blinded path embedded - -3. **Share the Offer (Without Revealing Identity)** - - Give the offer string to a friend (simulated payer) - - Their Lightning wallet decodes the offer - - They can pay without knowing your actual node ID - -4. **Analyze What the Payer Sees** - - Payer receives: `bolt12://...` - - Payer learns: A way to send you money, amount, description - - Payer does NOT learn: Your node ID, location, real identity - - **Question:** How is this different from BOLT11? - -5. **Send a Payment (Payer Perspective)** - - Create a second node (payer) - - Use the BOLT12 offer to create an invoice - - Command: `lightning-cli pay ` - - The payment routes through blinded paths - - Neither sender nor receiver knows each other's exact node identity - -6. **Verify Privacy** - - Check the payment in your node's database - - Check the payer's node: `lightning-cli listinvoices` - - Compare: What information is visible on each side? - - **Finding:** The payment succeeded, but the graph structure remains private - -#### Part 3: Understand Blinded Paths (90 minutes) - -1. **Blinded Path Concept** - - A path from a public node to your node, encrypted so intermediate nodes can't see the destination - - Format: `[node1 (encrypted→2), node2 (encrypted→3), node3 (encrypted→4), you]` - - Intermediate node 2 knows node 3, but not nodes 4 or 5 - - Result: Nobody except you knows the full path to you - -2. **Trace a Blinded Path** - - Create a 3-node blinded path manually: - - Alice (public) → Bob (intermediate) → Charlie (intermediate) → You (recipient) - - Alice sends payment using the blinded path - - Bob sees: Payment destined for an encrypted value, forwards to Charlie - - You see: Payment arrived, recognize the blinded path as yours - - **Key insight:** No intermediate node sees the full path - -3. **Compare to Source Routing** - - Classic Lightning: Sender specifies the full path - - Everyone on the path knows the full structure - - Blinded paths: Sender only sees the first hop, rest is encrypted - - Privacy improvement: Intermediate hops don't map the graph - -#### Part 4: Detect and Defend Against Channel Probing (90 minutes) - -1. **Understand Channel Probing Attack** - - Attacker sends small test payments to map your network topology - - Each test reveals: "Is this channel funded? How much? Who is the node?" - - Attacker can identify: Your channels, capacity, balance (approximately) - - **Risk:** An attacker could identify you as a merchant if your channel balances spike - -2. **Implement Probing Detection** - - Core Lightning has built-in defenses - - Enable: Accept channel probes but don't leak balance information - - Configure: `accept-htlc-underpaying-msat` to reject obviously fake payments - -3. **Create a Probing Detection Script** - - Monitor your Lightning node's failed HTLCs - - Pattern: Many small failed payments from the same node = likely probing - - Action: Block the probing node in your firewall rules - -4. **Privacy Best Practices for Lightning** - - Use BOLT12 instead of BOLT11 for merchant invoices - - Rebalance channels over Tor to hide IP address - - Don't advertise channel capacity - - Rotate node aliases - - Document and share: "Lightning Privacy Checklist" - -### Real-World Exercise: Map a Lightning Network Community - -1. Use a Lightning network analyzer (e.g., Amboss, 1ML) -2. Identify a merchant or hub in your region -3. Map their channels, capacity, and payment flow -4. Write: "Here's what privacy leaks I can identify" -5. Propose: "Here's how they could improve privacy with BOLT12" - -### Open a PR This Week - -**Choose one:** - -1. **Core Lightning BOLT12 Documentation** - - Repo: `ElementsProject/lightning` - - Contribute: Tutorial on BOLT12 for merchants - - Target: Step-by-step guide with examples - -2. **LDK BOLT12 Implementation** - - Repo: `lightningdevkit/rust-lightning` - - Contribute: Documentation, test cases, or examples - - Target: Help stabilize BOLT12 in LDK - -3. **Lightning Privacy Scorecard** - - Repo: Create new or contribute to `1ML` or Lightning analysis tools - - Contribute: A privacy analysis tool that scores nodes on BOLT12 usage, IP privacy, etc. - - Target: Help the Lightning community improve privacy - -### Daily Life Privacy Tip - -**If you receive Lightning payments as a merchant, use BOLT12 offers.** - -Simple steps: -1. Update Core Lightning or switch to a BOLT12-supporting wallet -2. Create a BOLT12 offer: `lightning-cli offer "My Business"` -3. Share the offer code (not your node ID) -4. Customers pay the offer; their wallet handles blinded paths -5. You receive payments invisibly - -Privacy gain: Customers can't map your Lightning topology. - -### Reading List - -1. **BOLT12 Specification** - - https://github.com/lightning/bolts/blob/master/12-offer.md - -2. **Core Lightning BOLT12 Guide** - - https://docs.corelightning.org/docs/offers - -3. **LDK BOLT12 Progress** - - https://github.com/lightningdevkit/rust-lightning - -4. **Blinded Paths Technical Details** - - "Route Blinding" in Lightning Network documentation - - https://github.com/lightning/bolts/pull/999 - -5. **Lightning Privacy Research** - - "Echoes of Silence: Analyzing Light Client Privacy on Bitcoin" - -6. **1ML Lightning Network Analysis** - - https://1ml.com - ---- - -## Phase 5 Capstone: Privacy Technique Comparison - -By the end of Phase 5, you understand: -- **CoinJoin:** Obvious on-chain pattern, strong anonymity set, fast, centralized coordination -- **CoinSwap:** Invisible on-chain pattern, stealth, slow, P2P market-based -- **eCash (Fedimint/Cashu):** Perfect privacy with trusted federation, off-chain, requires deposit/redemption -- **Lightning (BOLT12):** Instant, on-chain settlement, node-level privacy, real-time - -**Your challenge:** Design a full privacy architecture for a Bitcoin user's complete financial life: -- Accumulate Bitcoin (private initial purchase) -- Store Bitcoin (multisig, geographically distributed) -- Spend Bitcoin (at merchants, for privacy) -- Receive Bitcoin (payments from friends) -- Exchange Bitcoin (to fiat or other assets) - -Write a 1000-word "Privacy Architecture" proposal that uses techniques from Sessions 17-20 appropriately. Explain your choices. - ---- - -## How to Excel in Phase 5 - -1. **Read the specifications**, not just the summaries -2. **Run the tools** - hands-on execution teaches more than reading -3. **Contribute code** - you don't just learn; you ship -4. **Ask questions** - privacy is complex; engage with the community -5. **Think about trade-offs** - privacy is never free; understand what you're trading - -Your future work in Bitcoin privacy depends on mastering these techniques. Make Phase 5 count. diff --git a/privacy-track/phase-5-advanced/session-17-coinjoin/coinjoin_analysis.py b/privacy-track/phase-5-advanced/session-17-coinjoin/coinjoin_analysis.py deleted file mode 100644 index 11c07d5..0000000 --- a/privacy-track/phase-5-advanced/session-17-coinjoin/coinjoin_analysis.py +++ /dev/null @@ -1,450 +0,0 @@ -""" -CoinJoin Analysis Exercise: Detecting and Analyzing Privacy Transactions -=========================================================================== - -In this exercise, you will learn to: -1. Identify CoinJoin transactions by their structural patterns -2. Calculate anonymity sets (the number of possible mapping between inputs and outputs) -3. Detect potential sybil attacks and toxic change outputs -4. Simulate a CoinJoin construction process -5. Analyze how attackers can reduce privacy through input knowledge - -CoinJoin Concepts: -- CoinJoin is a protocol where multiple users combine their transactions into one -- The resulting transaction has multiple inputs and outputs -- Equal-value outputs create ambiguity: which input maps to which output? -- This ambiguity is the anonymity set - attacker can't determine the mapping -- Different CoinJoin implementations have different characteristics (Wasabi, JoinMarket) -- Toxic change: outputs of unexpected amounts reveal information about original inputs - -Why CoinJoin Matters: -- Traditional Bitcoin transactions are pseudonymous, not anonymous -- Observers can track coins through the blockchain using heuristics -- CoinJoin breaks the deterministic input-output mapping -- The larger the anonymity set, the stronger the privacy guarantee - -This exercise simulates transaction data. Real analysis would use libraries like -python-bitcoinlib or chaincodedata APIs to parse actual blockchain transactions. -""" - -from typing import List, Dict, Tuple, Set -from collections import Counter -import json - - -# ============================================================================ -# Sample Transaction Data -# ============================================================================ - -# Transaction format: { -# 'txid': str, -# 'inputs': [{'address': str, 'amount': float}], -# 'outputs': [{'address': str, 'amount': float}], -# 'description': str (for educational purposes) -# } - -SAMPLE_TRANSACTIONS = [ - # Normal transactions (non-CoinJoin) - { - 'txid': 'normal_001', - 'inputs': [{'address': 'input_a', 'amount': 1.5}], - 'outputs': [ - {'address': 'output_a', 'amount': 1.0}, - {'address': 'change_a', 'amount': 0.49} - ], - 'description': 'Simple single-input transaction with change' - }, - { - 'txid': 'normal_002', - 'inputs': [ - {'address': 'input_b', 'amount': 2.0}, - {'address': 'input_c', 'amount': 0.5} - ], - 'outputs': [ - {'address': 'output_b', 'amount': 2.2}, - {'address': 'change_b', 'amount': 0.29} - ], - 'description': 'Two-input transaction with change (not a CoinJoin)' - }, - { - 'txid': 'normal_003', - 'inputs': [ - {'address': 'input_d', 'amount': 3.0}, - {'address': 'input_e', 'amount': 1.5}, - {'address': 'input_f', 'amount': 0.75} - ], - 'outputs': [ - {'address': 'output_c', 'amount': 5.0}, - {'address': 'change_c', 'amount': 0.249} - ], - 'description': 'Three inputs, two outputs - amount mismatch suggests non-CoinJoin' - }, - { - 'txid': 'normal_004', - 'inputs': [{'address': 'input_g', 'amount': 10.0}], - 'outputs': [{'address': 'output_d', 'amount': 9.99}], - 'description': 'Single input to single output with fees' - }, - { - 'txid': 'normal_005', - 'inputs': [ - {'address': 'input_h', 'amount': 0.5}, - {'address': 'input_i', 'amount': 0.5}, - {'address': 'input_j', 'amount': 0.5} - ], - 'outputs': [ - {'address': 'output_e', 'amount': 1.45}, - {'address': 'change_d', 'amount': 0.04} - ], - 'description': 'Three equal inputs but outputs do not match pattern' - }, - - # CoinJoin transactions - { - 'txid': 'coinjoin_001_wasabi', - 'inputs': [ - {'address': 'user_a_input', 'amount': 1.0}, - {'address': 'user_b_input', 'amount': 1.5}, - {'address': 'user_c_input', 'amount': 0.8} - ], - 'outputs': [ - {'address': 'user_a_output', 'amount': 1.0}, - {'address': 'user_b_output', 'amount': 1.0}, - {'address': 'user_c_output', 'amount': 1.0}, - {'address': 'user_a_change', 'amount': 0.29}, - {'address': 'user_b_change', 'amount': 0.0}, - {'address': 'user_c_change', 'amount': 0.01} - ], - 'description': 'Wasabi CoinJoin: 3 users, equal outputs (1.0), toxic change' - }, - { - 'txid': 'coinjoin_002_joinmarket', - 'inputs': [ - {'address': 'maker_input', 'amount': 5.0}, - {'address': 'taker1_input', 'amount': 2.0}, - {'address': 'taker2_input', 'amount': 3.0} - ], - 'outputs': [ - {'address': 'maker_output', 'amount': 4.95}, - {'address': 'taker1_output', 'amount': 1.95}, - {'address': 'taker2_output', 'amount': 3.05}, - {'address': 'fee_output', 'amount': 0.05} - ], - 'description': 'JoinMarket CoinJoin: outputs vary, designed for plausible deniability' - }, - { - 'txid': 'coinjoin_003_toxic', - 'inputs': [ - {'address': 'user_x_input', 'amount': 0.75}, - {'address': 'user_y_input', 'amount': 1.25}, - {'address': 'user_z_input', 'amount': 0.5} - ], - 'outputs': [ - {'address': 'user_x_output', 'amount': 0.9}, - {'address': 'user_y_output', 'amount': 0.9}, - {'address': 'user_z_output', 'amount': 0.9}, - {'address': 'user_x_change', 'amount': 0.145}, - {'address': 'user_y_change', 'amount': 0.305}, - {'address': 'user_z_change', 'amount': 0.010} - ], - 'description': 'Equal-output CoinJoin but change amounts leaked input values' - } -] - - -# ============================================================================ -# Exercise Functions - Implement These! -# ============================================================================ - -def find_equal_outputs(tx: Dict) -> Dict[float, List[str]]: - """ - Find groups of equal-value outputs in a transaction. - - This is a strong CoinJoin indicator: if many outputs have identical amounts, - the transaction likely involves multiple participants trying to hide the - input-output mapping. - - Args: - tx: Transaction dict with 'outputs' list - - Returns: - Dict mapping amount -> list of output addresses with that amount - Only includes amounts that appear more than once - - Example: - If outputs are [1.0, 1.0, 1.0, 0.2], returns {1.0: [addr1, addr2, addr3]} - """ - # TODO: Implement this function - # Hint: Use Counter from collections to group by amount - pass - - -def detect_coinjoin(tx: Dict) -> Tuple[bool, float, str]: - """ - Determine if a transaction is likely a CoinJoin. - - Strategy: - 1. Find equal output groups - 2. If found, check if there are enough inputs and outputs - 3. Return (is_coinjoin, confidence, reasoning) - - Args: - tx: Transaction dict - - Returns: - Tuple of: - - is_coinjoin: bool - - confidence: float 0-1 indicating certainty - - reasoning: str explaining the detection - - Note: This is heuristic-based. Real-world analysis would use additional signals. - """ - # TODO: Implement this function - # Hint: Check for equal outputs and reasonable number of participants - pass - - -def calculate_anonymity_set(tx: Dict) -> int: - """ - Calculate the anonymity set size. - - The anonymity set is the number of possible ways to map inputs to equal-value - outputs. For n participants with equal outputs, the anonymity set size is n! - (factorial) from the attacker's perspective. - - However, we'll use a simpler metric: the number of equal-value outputs. - This represents how many possible recipients an observer cannot distinguish. - - Args: - tx: Transaction dict - - Returns: - Size of the largest equal-output group (or 0 if no equal outputs) - - Example: - 6 outputs of 1.0 BTC each -> anonymity set = 6 - This means 6! = 720 possible input-output mappings to the attacker - """ - # TODO: Implement this function - pass - - -def identify_toxic_change(tx: Dict, equal_amount: float) -> List[Tuple[str, float]]: - """ - Identify "toxic change" outputs that leak information about inputs. - - In a proper CoinJoin, change should be indistinguishable from real outputs. - Change that differs from the equal-output amount reveals input information: - - If change is small: input had small amount - - If change is large: input had large amount - - This "toxic change" reduces privacy if change can be linked to input addresses. - - Args: - tx: Transaction dict - equal_amount: The value of equal outputs (e.g., 1.0 BTC) - - Returns: - List of (address, amount) for outputs != equal_amount - - Example: - Outputs: [1.0, 1.0, 1.0, 0.15, 0.05] - equal_amount: 1.0 - Returns: [('addr4', 0.15), ('addr5', 0.05)] - """ - # TODO: Implement this function - pass - - -def simulate_coinjoin(users: List[Dict]) -> Tuple[Dict, Dict]: - """ - Simulate a CoinJoin construction given multiple users. - - Each user provides: - - 'inputs': list of {'amount': float} - - 'desired_output': float (target amount for mixed output) - - 'address': str (identifier) - - The function should: - 1. Calculate total input across all users - 2. Determine the equal output amount (maximum possible while keeping excess as change) - 3. Construct outputs: equal-output for each user, plus change - 4. Return the constructed transaction and metrics - - Args: - users: List of dicts with 'address', 'inputs' (list of amounts), 'desired_output' - - Returns: - Tuple of: - - tx: Constructed transaction dict - - metrics: dict with: - - 'equal_amount': the fixed output amount - - 'anonymity_set': number of equal outputs - - 'total_change': sum of all change - - 'avg_toxic_change_per_user': average change amount - - Example: - User A: 3 inputs [1.0, 0.5, 0.5] = 2.0, wants 1.0 output - User B: 2 inputs [1.5, 0.5] = 2.0, wants 1.0 output - User C: 1 input [2.0] = 2.0, wants 1.0 output - Total: 6.0 BTC - - Optimal output: 1.0 BTC each (3 outputs) = 3.0 BTC - Remaining for change: 3.0 BTC (fees assumed 0 for simplicity) - """ - # TODO: Implement this function - pass - - -def analyze_sybil_attack(tx: Dict, sybil_inputs: Set[str]) -> Dict: - """ - Analyze how a Sybil attack reduces privacy. - - A Sybil attacker controls multiple inputs in a transaction. If the attacker - knows which inputs they control, they can reduce the effective anonymity set. - - Example: - - Transaction has 10 equal outputs (anonymity set = 10) - - Attacker controls 3 of the 7 inputs - - Attacker knows these 3 aren't theirs, limiting possibilities - - Effective anonymity set from honest user's perspective degrades - - Args: - tx: Transaction dict - sybil_inputs: Set of input addresses controlled by the attacker - - Returns: - Dict with: - - 'original_anonymity_set': size without attack knowledge - - 'known_bad_inputs': count of inputs controlled by attacker - - 'reduced_anonymity': effective anonymity set for honest users - - 'privacy_loss_percent': % reduction in privacy - - Simplified calculation: privacy_loss = (sybil_inputs_count / total_inputs) * 100 - """ - # TODO: Implement this function - pass - - -# ============================================================================ -# Main Analysis Block -# ============================================================================ - -def main(): - """ - Run analysis on all sample transactions. - - This demonstrates the exercise concepts by processing each transaction - and printing detection results, privacy metrics, and insights. - """ - - print("=" * 80) - print("COINJOIN ANALYSIS EXERCISE") - print("=" * 80) - print() - - # Analyze each transaction - results = [] - for tx in SAMPLE_TRANSACTIONS: - print(f"Transaction: {tx['txid']}") - print(f"Description: {tx['description']}") - print(f"Inputs: {len(tx['inputs'])}, Outputs: {len(tx['outputs'])}") - - # Detect if it's a CoinJoin - is_coinjoin, confidence, reasoning = detect_coinjoin(tx) - print(f" CoinJoin: {is_coinjoin} (confidence: {confidence:.1%})") - print(f" Reasoning: {reasoning}") - - if is_coinjoin: - # Find equal outputs - equal_groups = find_equal_outputs(tx) - if equal_groups: - for amount, addresses in equal_groups.items(): - print(f" Equal outputs: {len(addresses)} × {amount} BTC") - - # Calculate anonymity set - anon_set = calculate_anonymity_set(tx) - print(f" Anonymity set size: {anon_set}") - - # Identify toxic change - toxic = identify_toxic_change(tx, amount) - if toxic: - print(f" Toxic change detected:") - for addr, amt in toxic: - print(f" - {addr}: {amt} BTC") - - print() - results.append({ - 'txid': tx['txid'], - 'is_coinjoin': is_coinjoin, - 'confidence': confidence - }) - - # Summary - print("=" * 80) - print("SUMMARY") - print("=" * 80) - coinjoin_count = sum(1 for r in results if r['is_coinjoin']) - print(f"Detected {coinjoin_count} CoinJoin transactions out of {len(results)}") - print() - - # Simulate a CoinJoin - print("=" * 80) - print("SIMULATED COINJOIN CONSTRUCTION") - print("=" * 80) - print() - - users_for_sim = [ - { - 'address': 'alice', - 'inputs': [{'amount': 1.5}, {'amount': 0.5}], - 'desired_output': 1.5 - }, - { - 'address': 'bob', - 'inputs': [{'amount': 2.0}], - 'desired_output': 1.5 - }, - { - 'address': 'charlie', - 'inputs': [{'amount': 1.2}, {'amount': 0.8}], - 'desired_output': 1.5 - } - ] - - # This will fail until you implement simulate_coinjoin - try: - sim_tx, sim_metrics = simulate_coinjoin(users_for_sim) - print(f"Constructed CoinJoin:") - print(f" Equal output amount: {sim_metrics['equal_amount']} BTC") - print(f" Anonymity set: {sim_metrics['anonymity_set']}") - print(f" Total change: {sim_metrics['total_change']} BTC") - print(f" Avg change per user: {sim_metrics['avg_toxic_change_per_user']:.4f} BTC") - print() - except Exception as e: - print(f"Simulation not yet implemented: {e}") - print() - - # Analyze Sybil attack - print("=" * 80) - print("SYBIL ATTACK ANALYSIS") - print("=" * 80) - print() - - # Use the Wasabi example - wasabi_tx = SAMPLE_TRANSACTIONS[5] # coinjoin_001_wasabi - sybil_inputs = {'user_a_input', 'user_c_input'} - - try: - sybil_analysis = analyze_sybil_attack(wasabi_tx, sybil_inputs) - print(f"Transaction: {wasabi_tx['txid']}") - print(f"Attacker controls {len(sybil_inputs)} inputs") - print(f" Original anonymity set: {sybil_analysis['original_anonymity_set']}") - print(f" Privacy loss: {sybil_analysis['privacy_loss_percent']:.1f}%") - print(f" Effective anonymity: {sybil_analysis['reduced_anonymity']}") - except Exception as e: - print(f"Sybil analysis not yet implemented: {e}") - - -if __name__ == '__main__': - main() diff --git a/privacy-track/phase-5-advanced/session-19-ecash/cashu_mint_exercise.md b/privacy-track/phase-5-advanced/session-19-ecash/cashu_mint_exercise.md deleted file mode 100644 index 632025d..0000000 --- a/privacy-track/phase-5-advanced/session-19-ecash/cashu_mint_exercise.md +++ /dev/null @@ -1,545 +0,0 @@ -# Cashu Mint Exercise: eCash Privacy on Bitcoin - -## Overview - -Chaumian eCash provides **perfect sender privacy** within a mint through blind signatures. Unlike on-chain transactions where all UTXOs are public, or Lightning where nodes see routing information, eCash transactions are completely opaque to the mint operator. - -**Blind signatures** allow a user to obtain a cryptographic signature on a value that the signer (mint) never sees in plaintext. This means: -- The mint signs a blinded commitment -- The user unblinds the signature -- The mint cannot link the original request to the redemption -- Perfect privacy between user and mint - -This exercise walks through setting up a Cashu mint, minting tokens, sending them peer-to-peer, and analyzing what information leaks at each step. - -## Prerequisites - -- Python 3.9 or higher -- `pip` package manager -- Docker (for Lightning backend) -- `git` for cloning repositories -- Basic command-line familiarity -- ~30 minutes for full exercise - -## Part 1: Set Up a Cashu Mint - -### Step 1a: Clone and Install Nutshell - -```bash -# Clone the Nutshell Cashu mint reference implementation -git clone https://github.com/cashubtc/nutshell.git -cd nutshell - -# Create a virtual environment -python3 -m venv venv -source venv/bin/activate # On Windows: venv\Scripts\activate - -# Install dependencies -pip install -e . -``` - -### Step 1b: Configure for Signet/Testnet - -Create a `.env` file in the nutshell directory: - -```bash -# .env configuration for Cashu mint on Bitcoin Signet -MINT_PRIVATE_KEY=your_generated_key_here -MINT_HOST=0.0.0.0 -MINT_PORT=3338 -MINT_NETWORK=signet -MINT_DATABASE_URL=sqlite:///./mint.db - -# Lightning backend (LND, CLN, or LDK) -MINT_LIGHTNING_BACKEND=lnbits -MINT_LNBITS_URL=http://localhost:5000 -MINT_LNBITS_KEY=your_lnbits_admin_key - -# Optional: Postgres for production -# MINT_DATABASE_URL=postgresql://user:password@localhost/cashu_mint -``` - -### Step 1c: Start the Mint - -```bash -# Run the Cashu mint -cashu mint - -# The mint will be available at http://localhost:3338 -# Check the API with: curl http://localhost:3338/v1/info -``` - -**Expected output:** -```json -{ - "name": "Nutshell", - "version": "0.15.0", - "pubkey": "02a1b2c3...", - "keyset_id": "00a1b2c3...", - "mint_url": "http://localhost:3338" -} -``` - -### Step 1d: Connect a Lightning Backend - -For development, use **LNbits** (simple self-hosted Lightning wallet): - -```bash -# In a new terminal, run LNbits -docker run -p 5000:5000 lnbits/lnbits:latest - -# Visit http://localhost:5000 -# Create a wallet, copy the admin key to your .env MINT_LNBITS_KEY -``` - -Alternatively, use: -- **LND** (Lightning Network Daemon): Full node with gRPC API -- **CLN** (Core Lightning): Minimal setup with REST API -- **LDK** (Rust Lightning implementation) - -## Part 2: Mint Tokens - -### Step 2a: Pay a Lightning Invoice - -Get a Cashu token by paying an invoice: - -```bash -# Request an invoice from the mint -curl -X POST http://localhost:3338/v1/mint/quote/lightning \ - -H "Content-Type: application/json" \ - -d '{"amount": 100, "unit": "sat"}' - -# Response: -# { -# "quote": "05d0c...", -# "request": "lnbc1000n1pj...", -# "paid": false, -# "created_at": 1716129600, -# "expiry": 1716216000 -# } - -# Pay the invoice with your Lightning wallet (e.g., LNbits) -# Then claim the tokens: -curl -X POST http://localhost:3338/v1/mint/bolt11 \ - -H "Content-Type: application/json" \ - -d '{"quote": "05d0c...", "outputs": [{"amount": 100, "blind_message": "..."}]}' -``` - -### Step 2b: Examine the Token Structure - -A Cashu token is a JSON object: - -```json -{ - "mint": "http://localhost:3338", - "proofs": [ - { - "amount": 100, - "secret": "abcd1234...", - "C": "02a1b2c3...", - "id": "00a1b2c3..." - } - ] -} -``` - -**Decoding the token:** -- `amount`: Satoshi value -- `secret`: User's random secret (blinding input) -- `C`: Blind-signed commitment (mint's signature on blinded value) -- `id`: Keyset ID (which mint key was used) -- `mint`: Mint URL (where to redeem) - -```bash -# Inspect a Cashu token -python3 -c " -import json -import base64 -token = 'cashu...' # Paste your token here -decoded = base64.urlsafe_b64decode(token + '==') -print(json.dumps(json.loads(decoded), indent=2)) -" -``` - -### Step 2c: Understanding the Blind Signature - -**What the mint signs vs what you receive:** - -1. **User creates:** Secret `x`, computes public `Y = hash_to_curve(x)` -2. **User blinds:** Blinded commitment `B' = Y + r*G` (r = random blinding factor) -3. **User sends to mint:** Only `B'` (mint never sees `x` or `Y`) -4. **Mint signs:** `C' = k * B'` (k = mint's private key) -5. **User unblinds:** `C = C' - r*K` (K = k*G = mint's public key) -6. **Result:** `C = k*Y` (valid signature on `Y` that mint never explicitly signed) - -**Why this provides privacy:** -- Mint receives `B'` at step 2, receives `C` at redemption -- But `B'` is mathematically unrelated to `C` (due to blinding) -- Mint cannot link the minting request to the redemption - -```python -# Simplified blind signature example (elliptic curve) -from coincurve import PrivateKey, PublicKey -import hashlib -import secrets - -# Setup -mint_privkey = PrivateKey(secrets.token_bytes(32)) -mint_pubkey = mint_privkey.public_key - -# User creates a secret and blinds it -user_secret = secrets.token_bytes(32) -Y = PrivateKey(hashlib.sha256(user_secret).digest()).public_key.point - -# User blinds the commitment -r = secrets.randbelow(2**256) -blinding_factor = PrivateKey(r.to_bytes(32, 'big')).public_key.point -B_blind = Y.add(blinding_factor) - -# Mint signs the blinded commitment -C_blind = mint_privkey.secret * B_blind # scalar multiplication - -# User unblinds -C_unblind = C_blind.add(blinding_factor.multiply(-r)) - -# Verify: C_unblind == mint_privkey.secret * Y -print("Signature valid:", C_unblind == mint_privkey.secret * Y) -``` - -## Part 3: Send Tokens Peer-to-Peer - -### Step 3a: Send Tokens (No Mint Interaction) - -One of Cashu's key features: **tokens can be transferred directly between users without the mint's knowledge.** - -```bash -# User A has token and wants to send to User B - -# Export token as string -TOKEN="cashu://..." - -# User B imports token into their wallet -# (In reality, use Cashu wallet like ENuts, Minibits, or Cashu.me) - -# The mint sees NOTHING during this transfer -``` - -### Step 3b: Receiver Redeems Tokens - -When User B spends the token, they must "swap" it for fresh tokens (essential for double-spend prevention): - -```bash -# User B calls /v1/swap endpoint -curl -X POST http://localhost:3338/v1/swap \ - -H "Content-Type: application/json" \ - -d '{ - "inputs": [{"amount": 100, "secret": "abcd1234...", "C": "02a1b2c3..."}], - "outputs": [{"amount": 100, "blind_message": "..."}] - }' - -# Response: New signed tokens (unlinked to original) -``` - -### Step 3c: Privacy Analysis - What the Mint Sees - -| Operation | User Identity | Amount | Linking? | Comment | -|-----------|---------------|--------|----------|---------| -| Minting | Known (pays invoice) | Known | Yes | Linked to Lightning payment | -| P2P Send | Unknown | Known | No | Mint sees nothing | -| Redemption (Swap) | Unknown | Known | No | Blinded signature hides which token | -| Melting | Known (withdraws to LN) | Known | Yes | Linked to withdrawal invoice | - -**Key insight:** Within a mint, the sender and receiver are perfectly private from the mint and each other (except for amounts). The mint only learns about users when they enter (via Lightning) or exit (via Lightning) the system. - -## Part 4: Privacy Analysis - -Create a detailed table showing information flow: - -``` -╔════════════════════╦═══════════════════╦══════════════╦═════════════════╗ -║ Operation ║ User Linked? ║ Amounts ║ Linking Data ║ -╠════════════════════╬═══════════════════╬══════════════╬═════════════════╣ -║ Lightning Invoice ║ Yes (payer) ║ Visible ║ Payment hash ║ -║ Mint (Keyset) ║ No ║ Visible ║ Blind signature ║ -║ P2P Token Transfer ║ No (off-chain) ║ Visible ║ None ║ -║ Swap/Redeem ║ No ║ Visible ║ Blind signature ║ -║ Melt (LN Payout) ║ Yes (payee) ║ Visible ║ Payment hash ║ -╚════════════════════╩═══════════════════╩══════════════╩═════════════════╝ -``` - -**Question:** Can a sophisticated mint operator link minting to redemption by analyzing timing or amounts? - -**Answer:** Partially-if amounts and timing are unique, some correlation is possible. This is why: -- **Mixing/coinjoin-style protocols** combine tokens across users -- **Fedimint** uses distributed threshold signatures (no single operator) -- Users should **split and recombine** tokens to avoid fingerprinting - -## Part 5: Double-Spend Prevention - -### Step 5a: Attempt a Double-Spend - -```bash -# Proof: attempt to swap the same token twice -TOKEN_SECRET="abcd1234..." -TOKEN_COMMITMENT="02a1b2c3..." - -# First swap (succeeds) -curl -X POST http://localhost:3338/v1/swap \ - -H "Content-Type: application/json" \ - -d "{\"inputs\": [{\"secret\": \"$TOKEN_SECRET\", \"C\": \"$TOKEN_COMMITMENT\"}]}" -# Response: New token issued - -# Second swap (same token, should fail) -curl -X POST http://localhost:3338/v1/swap \ - -H "Content-Type: application/json" \ - -d "{\"inputs\": [{\"secret\": \"$TOKEN_SECRET\", \"C\": \"$TOKEN_COMMITMENT\"}]}" -# Response: Error - token already redeemed -``` - -### Step 5b: How Double-Spend Prevention Works - -1. **Storage:** Mint maintains a database of spent `(secret, C)` pairs -2. **Check:** Before issuing new tokens, mint verifies the input isn't in the spent set -3. **Record:** After successful swap, mint marks `(secret, C)` as spent -4. **Failure mode:** If honest participant redeems offline and syncs with conflicting state, one redemption fails - -**Trade-off:** Double-spend prevention requires **online validation** (unlike on-chain Bitcoin which is offline-verifiable). This is the cost of privacy and scalability. - -## Part 6: The Blind Signature Math - -This section walks through the complete Chaumian blind signature protocol step-by-step. - -### Setup - -- **Mint:** Generates private key `k` (scalar), computes public key `K = k*G` (elliptic curve point) -- **User:** Wants a signature on value `x` that the mint never sees - -### The Protocol - -#### Step 1: User Creates a Secret - -``` -User generates: x ← {0, 1}^256 (random 256-bit secret) -Compute: Y = hash_to_curve(x) (map secret to elliptic curve point) -``` - -**Why?** `Y` is the commitment on which the mint will sign. The user keeps `x` secret. - -#### Step 2: User Blinds the Commitment - -``` -User generates: r ← {0, 1}^256 (random blinding factor) -Compute: B' = Y + r*G (add randomness to Y) -Send to Mint: B' (blinded commitment) -``` - -**Why?** The blinding factor `r*G` randomizes the commitment. The mint cannot compute `Y` from `B'` without knowing `r`. - -#### Step 3: Mint Signs the Blinded Commitment - -``` -Mint receives: B' -Compute: C' = k * B' (scalar multiplication with private key) -Send to User: C' -``` - -**Note:** The mint signs blindly-it has no idea what it's actually signing. - -#### Step 4: User Unblinds the Signature - -``` -User receives: C' -Compute: C = C' - r*K (subtract the blinding factor's effect) -Result: C = k*Y (valid signature on the user's secret Y) -``` - -**Verification:** -``` -C' - r*K -= k*B' - r*K -= k*(Y + r*G) - r*(k*G) -= k*Y + k*r*G - r*k*G -= k*Y ✓ -``` - -#### Step 5: Redemption (What the Mint Sees) - -``` -User later sends: (x, C) or (Y, C) -Mint verifies: k*Y == C (using public key K = k*G) -Mint marks (Y, C) as spent -``` - -**The Magic:** The mint performed step 3 on `B'`, but verifies step 5 on `Y`. - -- `B' = Y + r*G` (from blinding) -- `C' = k*B'` (what mint signed) -- `C = C' - r*K = k*Y` (what user unblinds) - -The blinding factor `r` ensures `B'` is statistically independent of `Y`, so: -- Mint cannot link `B'` (from minting) to `Y` (from redemption) -- **Perfect unlinkability** ✓ - -### Complete Example (Pseudocode) - -```python -# Toy example with simple elliptic curve (not cryptographically sound) -import hashlib -import secrets - -# Elliptic curve: secp256k1 (used in Cashu) -# G = generator point, n = order - -def hash_to_curve(data): - """Hash to a curve point (simplified)""" - h = hashlib.sha256(data).digest() - return deserialize_point(h) - -# === SETUP === -mint_privkey = 0x0123456789... # k -mint_pubkey = multiply(mint_privkey, G) # K = k*G - -# === MINTING === -# User side -user_secret = secrets.token_bytes(32) # x -Y = hash_to_curve(user_secret) -r = secrets.randbelow(n) -B_blind = point_add(Y, multiply(r, G)) # B' = Y + r*G - -# Send B_blind to mint -# Mint side -C_blind = multiply(mint_privkey, B_blind) # C' = k*B' - -# Send C_blind back to user -# User side -K = mint_pubkey -C = point_subtract(C_blind, multiply(r, K)) # C = C' - r*K - -# === REDEMPTION === -# User sends: (user_secret, C) -# Mint computes Y from user_secret: -Y_check = hash_to_curve(user_secret) -# Verify signature: -assert multiply(mint_privkey, Y_check) == C -# Mark as spent: spent_tokens.add((user_secret, C)) -``` - -### Why Blind Signatures Matter for Bitcoin Privacy - -1. **Horizontal privacy:** Receiver cannot prove to sender that sender paid them (mint is intermediary) -2. **Forward privacy:** Even if mint is compromised, old transactions cannot be linked -3. **Scalability:** Tokens are off-chain; no blockchain space needed -4. **Fungibility:** All tokens of same amount are indistinguishable - -## Discussion Questions - -### Trust & Assumptions - -1. **Trust Model:** - - Bitcoin (on-chain): No trusted third party needed - - Lightning: Hop nodes see routing info; payment must be atomic - - Cashu (single mint): Mint must not steal or rug; must maintain database - -2. **What are the minimal trust assumptions of eCash vs on-chain Bitcoin?** - - eCash: Mint doesn't steal, doesn't correlate users, doesn't censor - - On-chain: Network doesn't 51% attack, script execution is correct - -3. **Single Point of Failure:** - - If the mint goes offline, can you spend your tokens? (No, without swap) - - If the mint disappears, is your money gone? (Yes) - - How is this different from custodial exchanges? - -### Fedimint & Federation - -4. **How could a Fedimint (federation) reduce the single-mint trust assumption?** - - Instead of `k`, use threshold scheme: `k = k1 + k2 + k3 + ... + kN` - - M-of-N guardians must cooperate to sign - - No single operator can steal or link transactions - - Reference: [Fedimint whitepaper](https://fedimint.org) - -5. **What's the tradeoff of using a federation?** - - Increased complexity (M-of-N threshold signatures) - - Latency (more parties = slower coordination) - - But: Reduced trust in any one operator - -### Protocol Design - -6. **What happens if the mint goes offline?** - - Active tokens cannot be spent (no swap available) - - Tokens are no longer useful until mint comes back - - Compare to on-chain: Can always broadcast your UTXO - - Compare to Lightning: Can unilaterally close channels - -7. **Could the mint identify you by analyzing token timing or amounts?** - - Yes, if you always mint unique amounts or at predictable times - - Mitigation: Mix amounts, wait random intervals, use Fedimint - -### Practical Deployment - -8. **When should you use eCash vs on-chain vs Lightning?** - -| Scenario | Best Choice | Why | -|----------|------------|-----| -| Privacy within network | eCash | Perfect sender privacy | -| Cross-custody payments | On-chain | Trustless settlement | -| Instant payments | Lightning | Fast, scalable | -| Offline payments | On-chain | Only option that works | -| High throughput | eCash | Most efficient | - -9. **How does Cashu differ from traditional ecash (Digicash)?** - - Cashu: Multiple keysets, flexible denomination, Lightning-backed - - Digicash (1990s): Single operator, issued for fiat - - Both use Chaumian blind signatures - -10. **What's the relationship between Cashu and Fedimint?** - - Cashu: Single-operator or federated eCash - - Fedimint: Always federated (threshold signatures) - - Fedimint adds: eCash for privacy + threshold for decentralization - -## Troubleshooting - -**Mint won't start:** -```bash -# Check if port 3338 is in use -lsof -i :3338 -# Kill existing process if needed -kill -9 -``` - -**Lightning backend connection fails:** -- Verify LNbits is running: `curl http://localhost:5000` -- Check MINT_LNBITS_KEY in .env -- Ensure LN node has funds: visit LNbits UI - -**Token swap fails:** -- Verify token isn't already spent: `curl http://localhost:3338/v1/checkspendable` -- Check token format is valid JSON -- Ensure amounts match (no partial redeems without splitting) - -**Blind signature verification fails:** -- Verify you used correct keyset ID -- Check that curve operations are consistent (all secp256k1) -- Ensure C is a valid curve point - -## Next Steps - -1. **Explore Cashu ecosystem:** - - [Minibits wallet](https://minibits.cash) - - [ENuts wallet](https://enuts.cash) - - [Cashu.me](https://cashu.me) - -2. **Study advanced topics:** - - Chaumian CoinJoin (mixing eCash across users) - - Threshold cryptography (Fedimint) - - Privacy-preserving issuance protocols - -3. **Run your own mint:** - - Deploy Nutshell on testnet/signet - - Integrate with your Lightning node - - Build a web interface for token management - -4. **Read more:** - - [Cashu NUT specs](https://github.com/cashubtc/nuts) - - [David Chaum's blind signatures (1983)](https://www.chaum.com/publications/) - - [Fedimint documentation](https://docs.fedimint.org) - diff --git a/privacy-track/phase-6-contributing/README.md b/privacy-track/phase-6-contributing/README.md deleted file mode 100644 index 38ccd15..0000000 --- a/privacy-track/phase-6-contributing/README.md +++ /dev/null @@ -1,1108 +0,0 @@ -# Phase 6: Building & Contributing to Bitcoin Privacy -## Sessions 21-24 - -This phase shifts from learning to shipping. You'll build production-grade tools, contribute real code to major Bitcoin privacy projects, and deliver a capstone that demonstrates mastery. By the end, you'll have shipped code, contributed to the privacy ecosystem, and be ready for advanced fellowship work. - ---- - -## Session 21: Privacy-Preserving Wallet Development with BDK - -### Real-World Scenario - -You're building a wallet app for Bitcoin users who care about privacy. BDK (Bitcoin Development Kit) handles the hard parts: blockchain communication, UTXO selection, transaction building. But BDK is just a library. YOU must design it for privacy: - -- No address reuse (each payment gets a fresh address) -- Taproot-only (P2TR outputs hide script details) -- Coin control mandatory (users choose which coins to spend) -- No coin consolidation (don't merge UTXOs unless necessary) - -Your wallet is the difference between private and exposed Bitcoin. - -**This is wallet-level privacy engineering.** - -### Learning Objectives - -- Understand BDK's coin selection algorithm and how to customize it for privacy -- Design a wallet architecture that enforces privacy best practices -- Implement Taproot-only outputs (P2TR) -- Build a functional coin control UI -- Integrate change management for privacy -- Contribute production-grade code to BDK ecosystem - -### Privacy Tools You'll Use - -- **BDK** (Rust Bitcoin Development Kit) -- **bdk-cli** (command-line testing) -- **Sparrow Wallet** (reference implementation to study) -- **Test networks** (signet, testnet) -- **Bitcoin Core** (full node backend) - -### Detailed Hands-On Exercise - -#### Part 1: Design Your Privacy Wallet Architecture (90 minutes) - -1. **Study Sparrow Wallet** - - Download and run Sparrow Wallet - - Create a test wallet (testnet) - - Examine: How it displays UTXOs - - Examine: Coin control interface - - Examine: Change address handling - - Document: What privacy features does Sparrow implement? - -2. **Read the BDK Documentation** - - https://docs.rs/bdk/latest/bdk/ - - Focus: Wallet struct, coin selection, transaction building - - Understand: How BDK selects coins by default - - Understand: How to override coin selection - -3. **Design Your Privacy Wallet** - - Create a requirements document: - - Mandatory features: P2TR only, no address reuse, coin control UI - - Privacy properties: No consolidation heuristics, no round amounts - - UX: Simple for users, powerful for privacy-conscious users - - Draw: Wallet architecture diagram (key management, change, UTXOs, transaction building) - -4. **Privacy Requirements Checklist** - - Address reuse: Never. Each payment = new address - - Script types: Taproot (P2TR) only. No P2WPKH, no P2SH - - Coin control: User must select which UTXOs to spend - - Change: To a new address, never back to payment address - - Amounts: No rounding. Exact amounts to preserve privacy - - Fee selection: Avoid amounts that leak round values - - Document: Why each requirement matters - -#### Part 2: Implement a Privacy-First Wallet Skeleton in Rust with BDK (180 minutes) - -1. **Set Up Your Rust Project** - ```bash - cargo new privacy_wallet - cd privacy_wallet - cargo add bdk bdk-sqlite bdk-electrum tokio serde - ``` - -2. **Create the Core Wallet Structure** - ```rust - use bdk::{Wallet, blockchain::ElectrumBlockchain}; - use bdk::database::sqlite::SqliteDatabase; - - pub struct PrivacyWallet { - // Private key management - // UTXO tracking - // Change address generation - // Transaction builder - } - - impl PrivacyWallet { - pub fn new(extended_private_key: String, network: Network) -> Result { - // Initialize wallet with Taproot descriptor - // Set up SQLite database - // Connect to blockchain - } - - pub fn get_utxos(&self) -> Vec { - // Return all available UTXOs with metadata - } - - pub fn create_transaction( - &self, - recipient: Address, - amount: u64, - selected_utxos: Vec, - ) -> Result { - // Build transaction with selected UTXOs - // Generate new change address - // Validate privacy properties - } - } - ``` - -3. **Implement Mandatory Privacy Features** - - a. **Address Derivation (No Reuse)** - ```rust - pub fn generate_payment_address(&mut self) -> Address { - // Use BIP32 to derive next address - // Store derivation index - // Never reuse the same index - // Return fresh Taproot address - } - ``` - - b. **Taproot-Only Descriptors** - ```rust - // Force all descriptors to be P2TR (Taproot) - let descriptor = "tr([...])"; // Taproot descriptor - // Reject any non-Taproot descriptors at wallet creation - ``` - - c. **Coin Control Implementation** - ```rust - pub fn create_transaction_with_coin_control( - &self, - recipient: Address, - amount: u64, - selected_utxo_ids: Vec, // User selects which UTXOs to spend - ) -> Result { - // Verify user selection - // Build transaction with ONLY selected UTXOs - // Don't add "better" UTXOs automatically - // Return transaction with selected coins - } - ``` - - d. **Change Address Management** - ```rust - pub fn get_change_address(&mut self) -> Address { - // Generate a new, never-before-used address for change - // Use BIP32 derivation (different path from payment addresses) - // Store metadata: "this is change from tx_id" - // Return address that won't leak user privacy - } - ``` - -4. **Build the Transaction Manager** - ```rust - pub struct TransactionBuilder { - wallet: &PrivacyWallet, - } - - impl TransactionBuilder { - pub fn with_recipient(mut self, addr: Address, amount: u64) -> Self { - self.recipient = Some(addr); - self.amount = amount; - self - } - - pub fn with_utxos(mut self, utxos: Vec) -> Self { - self.selected_utxos = utxos; - self - } - - pub fn with_fee_rate(mut self, sats_per_vb: f32) -> Self { - self.fee_rate = sats_per_vb; - self - } - - pub fn build(self) -> Result { - // 1. Validate all UTXOs are Taproot - // 2. Calculate total input value - // 3. Estimate transaction size - // 4. Calculate fee - // 5. Generate change address - // 6. Build transaction - // 7. Verify: inputs + change = outputs + fee - } - } - ``` - -5. **Add Privacy Validation** - ```rust - pub fn validate_privacy(&self, tx: &Transaction) -> Result<()> { - // Check 1: All outputs are P2TR or P2WSH - // Check 2: Change address is fresh (never used) - // Check 3: No obvious patterns in amounts - // Check 4: Fee rate is reasonable (not suspiciously low or high) - // Check 5: No address reuse detected - return Ok(()); - } - ``` - -6. **Write Tests** - ```rust - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_address_never_reused() { - // Generate 10 addresses - // Verify all are unique - } - - #[test] - fn test_taproot_only() { - // Create transaction - // Verify all outputs are Taproot - } - - #[test] - fn test_coin_control_respected() { - // Select specific UTXOs - // Verify ONLY those UTXOs are spent - } - - #[test] - fn test_change_address_fresh() { - // Create multiple transactions - // Verify each uses a new change address - } - } - ``` - -#### Part 3: Test on Testnet (90 minutes) - -1. **Set Up a Testnet Environment** - - Create a testnet wallet - - Fund it with testnet Bitcoin (faucet) - - Wait for confirmations - -2. **Execute Privacy Transactions** - - Create a transaction with coin control - - Specify exact UTXOs to spend - - Set custom fee rate - - Broadcast to testnet - - Monitor on blockchain explorer - -3. **Analyze Your Transactions** - - Open blockchair.com or mempool.space - - Search your transaction - - Verify: All outputs are Taproot (no visible script type) - - Verify: Change address is fresh - - Verify: No obvious consolidation pattern - -4. **Document Your Implementation** - - Write a README for your wallet - - Include: Architecture diagram, privacy properties, coin control guide - - Publish to GitHub - -### Real-World Exercise: Contribute to BDK - -1. **Fork the BDK Repository** - - https://github.com/bitcoindevkit/bdk - - Clone locally - -2. **Choose a Contribution** - - Option 1: Add a privacy-focused coin selection example - - Option 2: Improve documentation for Taproot wallet setup - - Option 3: Write a privacy best practices guide for wallet developers - -3. **Create a Pull Request** - - Reference your privacy wallet implementation - - Explain the privacy improvements - - Include tests and examples - -### Open a PR This Week - -**Choose one:** - -1. **BDK Example: Privacy Wallet** - - Repo: `bitcoindevkit/bdk` - - Contribute: A complete example showing privacy-first wallet design - - Target: `examples/privacy_wallet.rs` demonstrating all best practices - -2. **Sparrow Wallet Documentation** - - Repo: `sparrowwallet/sparrow` - - Contribute: "Privacy Configuration Guide" for Sparrow users - - Target: Help users understand and use coin control effectively - -3. **Your Own Privacy Wallet** - - Repo: Create `your-github/privacy-wallet` - - Contribute: Full working wallet implementation - - Target: Open-source reference for developers - -### Daily Life Privacy Tip - -**When choosing a wallet, ask: Does it support coin control?** - -If yes: You can choose which UTXOs to spend. You have privacy power. -If no: The wallet chooses for you (often poorly). You have no privacy. - -Action today: -1. Open Sparrow Wallet -2. Load a test wallet -3. Go to Transactions tab -4. Try selecting specific UTXOs to spend -5. Notice how coin control changes the transaction - -### Reading List - -1. **BDK Documentation** - - https://docs.rs/bdk/ - - Focus: Wallet, blockchain, transaction building - -2. **Sparrow Wallet Privacy Guide** - - https://www.sparrowwallet.com/docs/ - - Study their coin control implementation - -3. **Bitcoin Script Types** - - "Taproot Guide" - https://bitcoinops.org/en/topics/taproot/ - - Why Taproot is better for privacy - -4. **BIP32 Derivation** - - "Hierarchical Deterministic Wallets" - BIP32 - - Understanding address generation - -5. **Coin Selection Algorithms** - - "Change Avoidance & Consolidation" - privacy trade-offs - ---- - -## Session 22: Building a Privacy Testing Tool - -### Real-World Scenario - -You receive Bitcoin at 10 different addresses. How private is your transaction history? A privacy scoring tool can analyze your address and give you a score: - -- Have you ever reused an address? (Privacy leak: -20 points) -- Do you consolidate UTXOs regularly? (Privacy leak: -15 points) -- Do you use round amounts (0.1, 1.0, 10.0)? (Heuristic leak: -10 points) -- Do you mix script types (P2WPKH + P2TR)? (Fingerprint leak: -5 points) -- Have you used more than 3 entities? (Cluster leak: -10 points) - -Score: 40/100. Privacy: "Compromised. Improve by: Using CoinJoin, enabling coin control, avoiding consolidation." - -**This is privacy feedback that drives behavior change.** - -### Learning Objectives - -- Design a privacy scoring algorithm (10+ metrics) -- Implement on-chain analysis to detect privacy leaks -- Build a web or CLI tool that scores Bitcoin addresses -- Understand the heuristics chain analysts use -- Create a user-friendly privacy report -- Contribute to the privacy tools ecosystem - -### Privacy Tools You'll Use - -- **Blockchain APIs** (Blockchair, blockchain.com, Mempool.space) -- **Python** (data analysis and tool building) -- **Flask or FastAPI** (web server, optional) -- **Matplotlib** (visualization) -- **GitHub** (open-source publishing) - -### Detailed Hands-On Exercise - -#### Part 1: Design a Privacy Scoring Framework (120 minutes) - -1. **Define 10 Privacy Metrics** - - Metric 1: **Address Reuse Detection** - - Score: -20 points if address has been reused - - Implementation: Check if address appears in multiple transactions as a recipient - - Privacy impact: High. Reuse is a major heuristic. - - Metric 2: **Consolidation Pattern** - - Score: -15 points if address consolidates many small UTXOs into one - - Implementation: Detect transactions with 5+ inputs and 1 output - - Privacy impact: High. Screams "wallet consolidation" - - Metric 3: **Change Detection** - - Score: -10 points if change address can be identified - - Implementation: Detect outputs smaller than others (usually change) - - Privacy impact: Medium. Leaks transaction flow. - - Metric 4: **Round Amounts** - - Score: -8 points per transaction with round amounts (0.1, 1.0, 10.0) - - Implementation: Detect outputs that are exact powers of 10 in satoshis - - Privacy impact: Medium. Suggests merchant, not random spending - - Metric 5: **Script Type Mixing** - - Score: -5 points if mixing P2PKH, P2WPKH, P2WSH, P2TR - - Implementation: Analyze all outputs, detect heterogeneity - - Privacy impact: Medium. Identifies wallet software - - Metric 6: **Timing Pattern** - - Score: -5 points if transactions cluster at specific times (9am daily) - - Implementation: Analyze transaction timestamps for periodicity - - Privacy impact: Low-Medium. Suggests automated payments - - Metric 7: **Dust Attack Vulnerability** - - Score: -3 points if address receives many tiny UTXOs (dust) - - Implementation: Count UTXOs < 5000 satoshis - - Privacy impact: Low. Indicates potential dust attack target - - Metric 8: **Entity Clustering** - - Score: -10 points per identified entity - - Implementation: Heuristic clustering (common inputs, change detection) - - Privacy impact: High. Maps who you transact with - - Metric 9: **CoinJoin Participation** - - Score: +20 points if address has been in a CoinJoin - - Implementation: Detect CoinJoin patterns (51+ equal outputs) - - Privacy impact: High positive. Shows privacy-conscious behavior - - Metric 10: **Age and Activity** - - Score: +5 points if address is older than 1 year - - Implementation: Analyze first transaction date - - Privacy impact: Medium. Older addresses tend to be more obscure - -2. **Design the Report Format** - ``` - Privacy Score: 42/100 - - Critical Issues (Fix First): - - Address reused 3 times (-20 points) - - Heavy consolidation detected (-15 points) - - Warnings (Improve): - - Round amounts used in 2 transactions (-8 points) - - Script type mixing (P2WPKH + P2TR) (-5 points) - - Positives: - - Participated in CoinJoin (+20 points) - - No dust attack vulnerability (+0 points) - - Recommendations: - 1. Stop reusing addresses immediately - 2. Use CoinJoin before spending to mix history - 3. Enable coin control to avoid consolidation - 4. Diversify script types to avoid fingerprinting - ``` - -#### Part 2: Implement the Privacy Scorer (Python) (150 minutes) - -1. **Set Up Your Python Project** - ```bash - mkdir privacy_scorer - cd privacy_scorer - python3 -m venv venv - source venv/bin/activate - pip install requests pandas matplotlib - ``` - -2. **Create the Core Analyzer** - ```python - import requests - from collections import defaultdict - - class PrivacyAnalyzer: - def __init__(self, address): - self.address = address - self.api_key = "your_blockchair_key" - self.transactions = [] - self.score = 0 - self.metrics = {} - - def fetch_transactions(self): - # Fetch all transactions for this address - # Use Blockchair API - # Store in self.transactions - pass - - def detect_address_reuse(self): - # Check if address appears as recipient in multiple txs - # Return count of reuses - pass - - def detect_consolidation(self): - # Find transactions with many inputs, few outputs - # Return list of consolidation transactions - pass - - def detect_change_address(self): - # For each transaction, identify likely change output - # Usually smallest output or sent to new address - # Return probability of each output being change - pass - - def detect_round_amounts(self): - # Find outputs that are round amounts - # 0.1 BTC, 1.0 BTC, 10.0 BTC, etc. - # Return count and list - pass - - def detect_script_mixing(self): - # Analyze all outputs - # Detect if using mixed script types - # Return diversity score - pass - - def detect_coinjoin_participation(self): - # Look for transactions with 50+ equal outputs - # Return list of CoinJoin transactions - pass - - def analyze(self): - # Run all analyses - # Calculate total score - # Generate report - pass - ``` - -3. **Calculate Privacy Score** - ```python - def calculate_score(self): - total_score = 100 - - # Metric 1: Address Reuse - reuses = self.detect_address_reuse() - if reuses > 0: - total_score -= min(20, reuses * 5) - self.metrics['address_reuse'] = reuses - - # Metric 2: Consolidation - consolidations = self.detect_consolidation() - if len(consolidations) > 0: - total_score -= 15 - self.metrics['consolidation'] = len(consolidations) - - # Metric 3: Change Detection - change_leaked = self.detect_change_address() - if change_leaked > 0.7: # High confidence change detection - total_score -= 10 - self.metrics['change_leaked'] = change_leaked - - # ... continue for all 10 metrics - - self.score = max(0, total_score) - return self.score - ``` - -4. **Generate Privacy Report** - ```python - def generate_report(self): - report = f""" - Privacy Score: {self.score}/100 - - Critical Issues: - """ - if self.metrics['address_reuse'] > 0: - report += f"- Address reused {self.metrics['address_reuse']} times\n" - - if self.metrics['consolidation'] > 0: - report += f"- Heavy consolidation detected ({len(self.metrics['consolidation'])} instances)\n" - - report += "\nRecommendations:\n" - report += "1. Use CoinJoin to mix your transaction history\n" - report += "2. Enable coin control in your wallet\n" - report += "3. Avoid consolidating UTXOs\n" - report += "4. Use Taproot addresses (P2TR)\n" - - return report - ``` - -#### Part 3: Build a Web Interface (Flask) (90 minutes) - -1. **Create a Simple Web App** - ```python - from flask import Flask, render_template, request - - app = Flask(__name__) - - @app.route('/') - def index(): - return render_template('index.html') - - @app.route('/analyze', methods=['POST']) - def analyze(): - address = request.form['address'] - analyzer = PrivacyAnalyzer(address) - analyzer.fetch_transactions() - analyzer.analyze() - - return render_template('report.html', - score=analyzer.score, - metrics=analyzer.metrics, - report=analyzer.generate_report() - ) - - if __name__ == '__main__': - app.run(debug=True) - ``` - -2. **Create Simple HTML Templates** - ```html - -
- - - -
- - -
Privacy Score: {{ score }}/100
-
- {{ report|safe }} -
- - ``` - -#### Part 4: Test Your Tool (60 minutes) - -1. **Test on Known Addresses** - - Analyze a known merchant address (low privacy, high reuse) - - Analyze a known CoinJoin output address (high privacy) - - Analyze your own test address - - Verify scores match expectations - -2. **Validate Metrics** - - Test address reuse detection - - Test consolidation detection - - Test CoinJoin pattern detection - - Verify all calculations are correct - -3. **Document Edge Cases** - - What happens if address has no transactions? - - What happens if address is very new? - - What happens if address has 10,000+ transactions? - -### Real-World Exercise: Analyze Real Bitcoin Addresses - -1. **Find High-Privacy Addresses** - - Search for CoinJoin outputs on OXT.me - - Analyze the privacy score - - Verify it's high - -2. **Find Low-Privacy Addresses** - - Search for merchant addresses (known address reuse) - - Analyze the privacy score - - Verify it's low - -3. **Write Case Studies** - - "Why Coinbase Deposit Addresses Score 10/100" - - "Why CoinJoin Outputs Score 95/100" - - "How to Improve Your Privacy Score from 20 to 80" - -### Open a PR This Week - -**Choose one:** - -1. **Create a New GitHub Repository** - - Repo: `your-github/bitcoin-privacy-scorer` - - Contribute: Full working tool (Python + Flask) - - Target: Open-source reference implementation, MIT license - -2. **Contribute to Existing Privacy Tool** - - Repo: Search "Bitcoin privacy" on GitHub - - Contribute: Add a privacy scoring feature - - Target: Help existing privacy tool communities - -3. **Publish a Blog Post** - - Title: "Privacy Scoring Framework for Bitcoin Addresses" - - Explain: Your 10 metrics, how they work, what they mean - - Include: Interactive examples, case studies - - Target: Bitcoin Magazine, Nakamoto Institute, or personal blog - -### Daily Life Privacy Tip - -**Before spending Bitcoin, run it through a privacy scorer.** - -Action: -1. Take one of your Bitcoin addresses -2. Use your privacy scorer tool (or my-balance.io if available) -3. Check your privacy score -4. If it's below 60, use CoinJoin before spending - -### Reading List - -1. **Privacy Heuristics** - - "Chain Analysis 101" - Chainalysis blog - - Understanding what analysts look for - -2. **Consolidation and Change Detection** - - https://oxt.me/ - - Real examples of privacy leaks - -3. **CoinJoin Pattern Recognition** - - "Identifying CoinJoin Transactions" - academic papers - - How to detect mixing patterns - -4. **Bitcoin Script Analysis** - - "Address Types and Privacy" - bitcoinops.org - ---- - -## Session 23: Contribution Sprint - Ship Code Together - -### Real-World Scenario - -You and 10 other developers are in a room. Eight Bitcoin privacy projects have open issues on the table: - -- BDK: "Improve Taproot wallet examples" -- Core Lightning: "Improve BOLT12 documentation" -- Fedimint: "Add federation setup tutorial" -- Wasabi Wallet: "Improve UX for new CoinJoin users" -- JoinMarket: "Add configuration guide for privacy" -- Bitcoin Core: "Privacy review of transaction selection" -- LDK: "BOLT12 implementation examples" -- Cashu: "Improve blind signature documentation" - -In 4 hours, your cohort will submit 5+ PRs. This is how open-source Bitcoin privacy gets built. - -**This is the real work.** - -### Learning Objectives - -- Work collaboratively on real Bitcoin privacy repositories -- Merge-ready code in a production environment -- Experience code review and iteration -- Build relationships with Bitcoin privacy maintainers -- Ship impact in a single session -- Celebrate contributions with the cohort - -### How This Session Works - -1. **Morning (1 hour): Issue Selection & Pairing** - - Facilitator presents 8+ open issues from privacy projects - - Each participant picks an issue - - Facilitator pairs people with expertise on mentorship - -2. **Midday (2 hours): Deep Work** - - Participants work heads-down - - Facilitator and mentors available for questions - - Goal: PR ready for submission - -3. **Afternoon (1 hour): Live Code Review & Polish** - - Each person presents their PR - - Cohort gives feedback - - Facilitator helps with final polish - - Verify: Tests pass, documentation is clear, code is mergeable - -4. **End of Session: Submit** - - All PRs submitted to projects - - Cohort celebrates together - - Document the impact - -### Open Issues to Target - -**Pick one:** - -1. **BDK: Taproot Wallet Example** - - Repo: `bitcoindevkit/bdk` - - Issue: Create a complete example showing privacy-first Taproot wallet - - PR Target: `examples/privacy_taproot_wallet.rs` - - Est. Time: 90-120 minutes - - Impact: High. Teaches developers best practices - -2. **Core Lightning: BOLT12 Tutorial** - - Repo: `ElementsProject/lightning` - - Issue: Write step-by-step BOLT12 guide for merchants - - PR Target: `doc/BOLT12_GUIDE.md` - - Est. Time: 60-90 minutes - - Impact: High. Drives BOLT12 adoption - -3. **Fedimint: Federation Setup Guide** - - Repo: `fedimint/fedimint` - - Issue: Create "Your First Federation" tutorial - - PR Target: `docs/SETUP_GUIDE.md` - - Est. Time: 90-120 minutes - - Impact: High. Enables communities to self-host - -4. **Wasabi Wallet: CoinJoin Onboarding** - - Repo: `zkSNACKs/WalletWasabi` - - Issue: Improve UX for first-time CoinJoin users - - PR Target: UI improvements or documentation - - Est. Time: 120-150 minutes - - Impact: High. Better onboarding = more users - -5. **JoinMarket: Privacy Configuration Guide** - - Repo: `joinmarket-org/joinmarket-clientserver` - - Issue: Create configuration guide for yield generators - - PR Target: `docs/PRIVACY_CONFIG.md` - - Est. Time: 60-90 minutes - - Impact: Medium. Better market liquidity - -6. **Bitcoin Core: Transaction Privacy** - - Repo: `bitcoin/bitcoin` - - Issue: Add privacy considerations to coin selection docs - - PR Target: `doc/design/privacy.md` - - Est. Time: 90 minutes - - Impact: High. Influences Core development - -7. **LDK: BOLT12 Implementation Examples** - - Repo: `lightningdevkit/rust-lightning` - - Issue: Create BOLT12 example for LDK users - - PR Target: `lightning/examples/bolt12.rs` - - Est. Time: 120 minutes - - Impact: High. Enables developers to use BOLT12 - -8. **Cashu: Blind Signature Documentation** - - Repo: `cashubtc/nutshell` - - Issue: Improve blind signature protocol documentation - - PR Target: `docs/BLIND_SIGNATURES.md` - - Est. Time: 60 minutes - - Impact: Medium. Teaches cryptography - -### Working as a Cohort - -**You're not alone. Here's how this works:** - -- **Mentors available**: Real maintainers or experienced contributors -- **Code review loop**: Quick feedback, iterate, improve -- **Peer support**: Ask each other questions -- **Celebration**: Every PR counts. Every contribution matters. - -### Success Criteria - -- PR is submitted to the project's repository -- PR has clear description and rationale -- Code passes project's tests -- Documentation is clear and correct -- No obvious issues in code review - -### Open a PR This Week - -**You will submit a PR in this session.** No exceptions. The goal is 5+ PRs from your cohort. - -### Daily Life Privacy Tip - -**Contributing to Bitcoin privacy is how you level up faster than any tutorial.** - -Real developers learn by shipping. Ship your first PR today. - -### Reading List - -1. **Bitcoin Open Source Contribution Workflow** - - How to fork, branch, commit, and PR - - Standard Git workflow - -2. **Code Review Best Practices** - - How to respond to feedback - - How to iterate on PRs - -3. **Bitcoin Development Standards** - - Code style guides for major projects - - Testing requirements - -4. **License Compliance** - - MIT, Apache, GPLv3 basics - - How to respect copyright - ---- - -## Session 24: Capstone - Present Your Work, Apply for Fellowship - -### Real-World Scenario - -You're standing in front of 20 Bitcoin developers, 5 privacy researchers, and OpenSats fellowship directors. You have 15 minutes. - -"Here's what I built. Here are the 3 PRs I shipped. This is why Bitcoin privacy matters to me. This is how I'll contribute in the next 12 months." - -If you nail this presentation, you walk out with: -- Recognition from the Bitcoin privacy community -- Connections to mentors and developers -- A fellowship offer (best-case scenario) -- A clear roadmap for the next year - -**This is your moment.** - -### Learning Objectives - -- Communicate your work clearly and compellingly -- Tell the story of your privacy contributions -- Build relationships with Bitcoin privacy leaders -- Apply for advanced fellowships -- Plan your next 12 months of work -- Graduate as a Bitcoin privacy developer - -### How the Session Works - -1. **Morning (1 hour): Presentation Prep** - - Each participant prepares a 15-minute talk - - Topics: What you built, what you learned, what's next - - Practice with feedback from facilitators - -2. **Midday (1 hour): Guest Speakers** - - Invited speakers from Bitcoin privacy community - - Potential: josibake (protocol developer), Dan Gould (CoinJoin), 0xB10C (privacy researcher) - - Topic: "Working in Bitcoin Privacy: Careers, Fellowship, Open Questions" - -3. **Afternoon (2 hours): Cohort Presentations** - - Each participant presents their work - - 15 minutes talk + 5 minutes Q&A - - Audience: Developers, researchers, fellowship directors - - Recorded (with permission) for portfolio - -4. **Evening: Fellowship Application Workshop** - - Help participants apply for OpenSats fellowships - - Review applications - - Discuss next steps - -### Your Capstone Presentation (15 minutes) - -**Structure:** - -1. **Opening (1 minute)** - - "My name is X. I'm a Bitcoin privacy developer." - - Why privacy matters to you - -2. **What I Built (5 minutes)** - - Phase 5-6 capstone project - - Feature walkthrough - - Live demo (if possible) - -3. **Open Source Contributions (4 minutes)** - - 3+ PRs you shipped - - Links to repositories - - What each PR achieved - -4. **What I Learned (3 minutes)** - - Top 3 insights from the Privacy Track - - How thinking changed - - What's still confusing - -5. **Next 12 Months (2 minutes)** - - Where you want to contribute - - Specific projects in mind - - How OpenSats fellowship would help - -### Capstone Project Requirements - -By Session 24, you must have: - -1. **Built one complete project** (from Phase 5-6): - - Privacy wallet with BDK - - Privacy scoring tool - - CoinJoin analysis tool - - BOLT12 merchant setup - - Fedimint federation setup - - Or similar in scope and impact - -2. **Shipped 3+ PRs** to major Bitcoin projects: - - Wasabi, JoinMarket, Core Lightning, LDK, BDK, Bitcoin Core, Fedimint, Cashu - - PRs must be real, merged (or actively in review) - - Code you wrote, not just documentation - -3. **Documented your work**: - - GitHub repository with README - - Blog post or write-up - - Clear attribution to what you built - -4. **Created a portfolio** showing: - - Your capstone project - - Links to your 3+ merged PRs - - Your contributions to Bitcoin privacy - - Your vision for the next 12 months - -### Fellowship Application - -**What you'll apply for:** - -- **OpenSats Bitcoin Privacy Fellowship** - - $40k/year (typical) - - 12-month program - - Mentorship from privacy experts - - Access to Bitcoin Core, Lightning, privacy developer communities - - Apply at: https://www.opensats.org/ - -**What reviewers look for:** - -- Shipped code (PRs merged or in review) -- Clear communication about privacy problems -- Track record of contribution -- Passion and depth of knowledge -- Specific plans for 12 months -- Community endorsements (mentors, reviewers) - -**Your application includes:** - -- Capstone project (GitHub link) -- 3+ merged PRs (links) -- 500-word proposal: "My Work in Bitcoin Privacy" -- 3 references (facilitator, mentor, maintainer) -- 15-minute video presentation - -### Guest Speakers - -**Potential speakers from Bitcoin privacy community:** - -- **josibake** (Protocol & CoinJoin) - - Topic: "CoinJoin Protocol Design & Evolution" -- **Dan Gould** (CoinJoin + Markets) - - Topic: "Building Market-Based Privacy Infrastructure" -- **0xB10C** (Privacy Analysis & Metrics) - - Topic: "Measuring Privacy: Anonset, Metrics, and Heuristics" -- **Obi Nwosu** (Fedimint & eCash) - - Topic: "Community Privacy: Fedimint for Self-Hosted Bitcoin" -- **Chris Belcher** (CoinSwap, JoinMarket) - - Topic: "The Future of Bitcoin Privacy: CoinSwap and Beyond" - -### Open a PR This Week - -**No new PRs this week.** Focus on your presentation and application. - -### Daily Life Privacy Tip - -**Privacy is not a destination. It's a practice.** - -As you graduate from this track: -1. Use CoinJoin on your Bitcoin every quarter -2. Enable coin control in your wallet -3. Never reuse addresses -4. Follow Bitcoin privacy research -5. Contribute back to the projects you use - -### Reading List - -1. **Bitcoin Privacy Philosophy** - - "Bitcoin Privacy: Problems and Opportunities" - various authors - - Why privacy matters - -2. **OpenSats Fellowship Guidelines** - - https://www.opensats.org/ - - What they look for - -3. **Bitcoin Privacy Landscape** - - Current state of privacy tools - - Outstanding problems - -4. **Career in Bitcoin Privacy** - - Options: Protocol development, privacy tools, research, education - - How to sustain yourself while contributing - ---- - -## Phase 6 Capstone Presentation Checklist - -Before you present, verify: - -- Your capstone project works and is documented -- All 3+ PRs are real and verifiable -- Your presentation is under 15 minutes -- You have a live demo (or screenshots) -- Your slides are clear and professional -- You've practiced with feedback -- Your fellowship application is started -- You have 3 references lined up -- Your GitHub profile shows your work -- You're ready to answer tough questions - ---- - -## Congratulations! - -If you've completed Phases 1-6, you are now a Bitcoin privacy developer. - -You understand: -- How chain analysis works (and how to resist it) -- Every major privacy technique (Silent Payments, PayJoin, CoinJoin, CoinSwap, eCash, Lightning) -- How to build privacy-preserving tools -- How to contribute to major Bitcoin projects -- How to think about privacy trade-offs - -You've shipped code. You've contributed to open source. You've mastered the most important privacy techniques in Bitcoin. - -The next step? Keep building. Keep contributing. Keep advancing Bitcoin privacy. - -Welcome to the Bitcoin privacy developer community. - ---- - -## Resources for Continued Learning - -**After Phase 6:** - -1. **Bitcoin Privacy Research Community** - - Workshops at Bitcoin conferences - - Privacy-focused hackathons - - Academic papers and discussions - -2. **Advanced Topics** - - Multiparty computation (MPC) - - Zero-knowledge proofs (ZKPs) for privacy - - Advanced cryptography - - Protocol design - -3. **Maintainer Tracks** - - After 12 months as a developer, consider becoming a maintainer - - Maintain Wasabi, JoinMarket, or similar - - Lead protocol development - -4. **Teaching Track** - - After 2 years, teach the next cohort - - Become a Code Orange Faculty member - - Expand Bitcoin privacy education - ---- - -**The Bitcoin privacy movement needs you. Build great things.** diff --git a/privacy-track/phase-6-contributing/session-22-privacy-testing/privacy_scorer.py b/privacy-track/phase-6-contributing/session-22-privacy-testing/privacy_scorer.py deleted file mode 100644 index ef2c3cd..0000000 --- a/privacy-track/phase-6-contributing/session-22-privacy-testing/privacy_scorer.py +++ /dev/null @@ -1,515 +0,0 @@ -""" -Bitcoin Transaction Privacy Scoring Tool -========================================== - -This exercise teaches students to evaluate the privacy characteristics of Bitcoin transactions. -Students build a comprehensive scoring system that identifies privacy leaks and helps users -understand the tradeoffs in their transaction construction. - -The scoring system evaluates transactions across 10 different privacy dimensions: -1. Address reuse (major leak) -2. Script type mixing (moderate leak) -3. Round amounts (minor leak) -4. Change detection (major leak) -5. Fee fingerprinting (minor leak) -6. Broadcast timing (minor leak) -7. Locktime analysis (minor leak) -8. Unnecessary inputs (moderate leak) -9. Output ordering (minor leak) -10. Overall scoring (aggregate 0-100) - -Learning Objectives: -- Understand the various privacy vectors in Bitcoin transactions -- Implement heuristic-based privacy analysis -- Learn how to score and rank privacy concerns -- Build a user-friendly privacy report system - -Final score ranges from 0 (completely private) to 100 (highly identifiable). -""" - -from typing import List, Dict, Tuple, Set -from dataclasses import dataclass -from enum import Enum - - -class PrivacyLevel(Enum): - """Privacy level categories.""" - EXCELLENT = "Excellent" - GOOD = "Good" - FAIR = "Fair" - POOR = "Poor" - CRITICAL = "Critical" - - -@dataclass -class PrivacyFinding: - """Represents a single privacy issue found in a transaction.""" - category: str - issue: str - points_lost: int - severity: str # "low", "medium", "high", "critical" - recommendation: str - - -@dataclass -class PrivacyReport: - """Complete privacy analysis report for a transaction.""" - transaction_id: str - findings: List[PrivacyFinding] - score: int # 0-100, lower is more private - privacy_level: PrivacyLevel - summary: str - recommendations: List[str] - - -# Sample transactions for testing and learning -SAMPLE_TRANSACTIONS = [ - { - "id": "tx_001", - "inputs": [ - {"address": "bc1qxyz123", "amount": 50000, "script_type": "P2WPKH"}, - ], - "outputs": [ - {"address": "bc1qabc123", "amount": 25000, "script_type": "P2WPKH"}, - {"address": "bc1qdef456", "amount": 24500, "script_type": "P2WPKH"}, - ], - "fee_rate": 2.5, - "broadcast_time": 1400, # 14:00 UTC (predictable business hours) - "locktime": 0, - "address_history": {"bc1qxyz123": 5, "bc1qabc123": 0, "bc1qdef456": 0}, - }, - { - "id": "tx_002", - "inputs": [ - {"address": "1ABC1234", "amount": 100000, "script_type": "P2PKH"}, - ], - "outputs": [ - {"address": "1DEF5678", "amount": 50000, "script_type": "P2PKH"}, - {"address": "1GHI9012", "amount": 50000, "script_type": "P2PKH"}, - ], - "fee_rate": 1.0, - "broadcast_time": 340, # 03:40 UTC (random time) - "locktime": 856000, - "address_history": {"1ABC1234": 12, "1DEF5678": 0, "1GHI9012": 0}, - }, - { - "id": "tx_003", - "inputs": [ - {"address": "bc1qaa111", "amount": 75000, "script_type": "P2WPKH"}, - {"address": "bc1qbb222", "amount": 50000, "script_type": "P2WPKH"}, - ], - "outputs": [ - {"address": "bc1qcc333", "amount": 50000, "script_type": "P2WPKH"}, - {"address": "bc1qdd444", "amount": 75000, "script_type": "P2WPKH"}, - ], - "fee_rate": 3.333, - "broadcast_time": 1200, - "locktime": 0, - "address_history": {"bc1qcc333": 8, "bc1qdd444": 0}, - }, - { - "id": "tx_004", - "inputs": [ - {"address": "bc1qprivate1", "amount": 100000, "script_type": "P2WSH"}, - ], - "outputs": [ - {"address": "bc1qnew001", "amount": 99567, "script_type": "P2WSH"}, - ], - "fee_rate": 4.33, - "broadcast_time": 245, # 02:45 UTC - "locktime": 0, - "address_history": {"bc1qprivate1": 0, "bc1qnew001": 0}, - }, - { - "id": "tx_005", - "inputs": [ - {"address": "bc1qmixed1", "amount": 100000, "script_type": "P2WPKH"}, - {"address": "3Mix1111", "amount": 50000, "script_type": "P2SH"}, - ], - "outputs": [ - {"address": "1Legacy111", "amount": 75000, "script_type": "P2PKH"}, - {"address": "bc1qchange1", "amount": 74500, "script_type": "P2WPKH"}, - ], - "fee_rate": 2.0, - "broadcast_time": 1800, - "locktime": 0, - "address_history": { - "bc1qmixed1": 0, - "3Mix1111": 0, - "1Legacy111": 0, - "bc1qchange1": 0, - }, - }, -] - - -def check_address_reuse(tx: Dict) -> Tuple[int, str]: - """ - Check if transaction addresses have been used before (address reuse vulnerability). - - Address reuse is a critical privacy leak as it directly links transactions together - on the public blockchain. Each address should ideally be used only once. - - Points deducted: up to 20 pts (each reused address = 5 pts) - - Args: - tx: Transaction dict with 'address_history' field - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement address reuse detection - # Hint: Check address_history values. Count reused addresses (history > 0) - # Deduct 5 points per reused address, up to 20 maximum - pass - - -def check_script_type_mixing(tx: Dict) -> Tuple[int, str]: - """ - Check if transaction mixes different script types (P2PKH, P2WPKH, P2SH, P2WSH, P2TR). - - Mixing script types can leak information about wallet capabilities and create - identifiable patterns. Consistent script type usage is more private. - - Points deducted: 0-15 pts - - Args: - tx: Transaction dict with inputs/outputs having 'script_type' field - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement script type mixing detection - # Hint: Collect all script types from inputs and outputs - # If more than one type is used, deduct points proportional to mixing - pass - - -def check_round_amounts(tx: Dict) -> Tuple[int, str]: - """ - Check if output amounts are suspiciously round (100000, 50000, 1000000, etc). - - Round amounts (divisible by 10000, 100000, etc.) are easier to identify and - fingerprint. More random/specific amounts provide better privacy. - - Points deducted: 0-10 pts - - Args: - tx: Transaction dict with outputs having 'amount' field - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement round amount detection - # Hint: Check if output amounts end in zeros (divisible by 10000, 100000, etc.) - # Deduct points based on how many outputs are suspiciously round - pass - - -def check_change_detection(tx: Dict) -> Tuple[int, str]: - """ - Check if the change output can be easily identified using heuristics. - - Change detection is critical for privacy. If an attacker can identify which - output is change, they can link the recipient address to the sender. - - Common heuristics: - - Smallest output is change - - Largest output is change - - Only new address is change - - Specific ordering pattern - - Points deducted: 0-15 pts - - Args: - tx: Transaction dict with inputs/outputs - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement change detection analysis - # Hint: Use multiple heuristics to score how easily change can be identified - # Consider: size differences, address reuse, ordering - pass - - -def check_fee_fingerprint(tx: Dict) -> Tuple[int, str]: - """ - Check if the fee rate matches known wallet implementations. - - Certain fee rates (1.0, 2.0, 5.0, etc.) are characteristic of specific wallets. - Custom or unusual fee rates provide some privacy against fingerprinting. - - Points deducted: 0-5 pts - - Args: - tx: Transaction dict with 'fee_rate' field - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement fee fingerprinting detection - # Hint: Check if fee_rate is a round number (1, 2, 5, 10, etc.) - # These are highly recognizable. Precise rates like 3.33 are better. - pass - - -def check_timing(tx: Dict) -> Tuple[int, str]: - """ - Check if transaction was broadcast at a predictable/suspicious time. - - Broadcasting at 14:00 UTC every time suggests a human or automated schedule. - Random times at night (02:45) are more typical of automated/privacy-conscious tx. - - Points deducted: 0-5 pts - - Args: - tx: Transaction dict with 'broadcast_time' field (minutes since midnight UTC) - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement broadcast timing analysis - # Hint: Business hours (8:00-18:00 UTC = 480-1080) are more suspicious - # Random night times are less suspicious - pass - - -def check_locktime(tx: Dict) -> Tuple[int, str]: - """ - Check if locktime reveals wallet implementation details. - - Locktime patterns can fingerprint wallets: - - Bitcoin Core: Recent block height (anti-fee-sniping) - - Most others: locktime = 0 - - Some wallets: Random values - - Points deducted: 0-5 pts - - Args: - tx: Transaction dict with 'locktime' field - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement locktime analysis - # Hint: Recent block heights (>500000) suggest Bitcoin Core - # Zero locktime is neutral. Very old values are suspicious. - pass - - -def check_unnecessary_input(tx: Dict) -> Tuple[int, str]: - """ - Check if transaction uses more inputs than necessary (inefficient UTXO selection). - - Using more inputs than needed can leak information about wallet state and - increase privacy risks through blockchain analysis. - - Points deducted: 0-10 pts - - Args: - tx: Transaction dict with inputs/outputs - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement unnecessary input detection - # Hint: Compare total input value vs total output value + reasonable fee - # Excess inputs (where one input alone would suffice) loses points - pass - - -def check_output_ordering(tx: Dict) -> Tuple[int, str]: - """ - Check if output ordering follows wallet-specific patterns. - - Some wallets use: - - Default ordering (as specified by user) - - BIP69 lexicographic ordering (deterministic) - - Random/shuffled ordering - - Recognizable patterns lose privacy points. - - Points deducted: 0-5 pts - - Args: - tx: Transaction dict with outputs - - Returns: - Tuple of (points_lost, description) - """ - # TODO: Implement output ordering analysis - # Hint: Check if outputs are sorted or follow a recognizable pattern - pass - - -def overall_privacy_score(tx: Dict, findings: List[PrivacyFinding]) -> int: - """ - Calculate the overall privacy score aggregating all findings. - - Score ranges from 0-100: - - 0-20: Excellent privacy - - 21-40: Good privacy - - 41-60: Fair privacy - - 61-80: Poor privacy - - 81-100: Critical privacy issues - - Args: - tx: Transaction being scored - findings: List of all privacy findings - - Returns: - Overall score (0-100) - """ - # TODO: Implement overall scoring - # Hint: Sum all points_lost from findings, cap at 100 - pass - - -def generate_report(tx: Dict) -> PrivacyReport: - """ - Generate a complete privacy report for a transaction. - - Coordinates all individual checks and produces a structured report with - findings, score, privacy level, and recommendations. - - Args: - tx: Transaction to analyze - - Returns: - PrivacyReport object with complete analysis - """ - # TODO: Implement report generation - # Hint: - # 1. Call each check_* function to collect findings - # 2. Calculate overall score - # 3. Determine privacy level based on score - # 4. Generate recommendations based on findings - # 5. Return populated PrivacyReport - pass - - -def print_report(report: PrivacyReport) -> None: - """ - Pretty-print a privacy report for user review. - - Args: - report: PrivacyReport to display - """ - print(f"\n{'=' * 75}") - print(f"PRIVACY ANALYSIS REPORT: {report.transaction_id}") - print(f"{'=' * 75}") - print() - - # Display score and privacy level - print(f"OVERALL SCORE: {report.score}/100") - print(f"PRIVACY LEVEL: {report.privacy_level.value}") - print() - - if report.score <= 20: - emoji = "✓" - elif report.score <= 40: - emoji = "~" - elif report.score <= 60: - emoji = "!" - else: - emoji = "✗" - - print(f"Summary: {emoji} {report.summary}") - print() - - # Display findings - if report.findings: - print("FINDINGS:") - print("-" * 75) - for finding in report.findings: - severity_indicator = { - "low": "[*]", - "medium": "[!]", - "high": "[!!]", - "critical": "[!!!]", - }.get(finding.severity, "[?]") - - print( - f"{severity_indicator} {finding.category}: {finding.issue} (-{finding.points_lost} pts)" - ) - print(f" Recommendation: {finding.recommendation}") - print() - else: - print("No significant privacy issues detected!") - print() - - # Display recommendations - if report.recommendations: - print("RECOMMENDATIONS FOR IMPROVEMENT:") - print("-" * 75) - for i, rec in enumerate(report.recommendations, 1): - print(f"{i}. {rec}") - print() - - print(f"{'=' * 75}\n") - - -def main(): - """ - Main function - demonstrates privacy scoring on sample transactions. - """ - print("\n" + "=" * 75) - print("BITCOIN TRANSACTION PRIVACY SCORER") - print("=" * 75) - print() - print("This tool evaluates the privacy characteristics of Bitcoin transactions") - print("by analyzing 10 different privacy vectors and assigning a comprehensive score.") - print() - - # Analyze each sample transaction - for tx in SAMPLE_TRANSACTIONS: - report = generate_report(tx) - if report: - print_report(report) - else: - print(f"Unable to generate report for {tx['id']}") - - # Summary statistics - print("=" * 75) - print("ANALYSIS COMPLETE") - print("=" * 75) - print() - print("Privacy Tips for Better Bitcoin Transactions:") - print() - print("1. ADDRESS REUSE") - print(" - Use a new address for each transaction") - print(" - Maintain a wallet that generates fresh addresses automatically") - print() - print("2. SCRIPT TYPE CONSISTENCY") - print(" - Stick to one script type (P2WPKH for modern wallets)") - print(" - Avoid mixing P2PKH, P2SH, and P2WPKH in same transaction") - print() - print("3. AMOUNT SELECTION") - print(" - Use precise amounts instead of round numbers") - print(" - Avoid round values like 100000, 50000, 1000000 satoshis") - print() - print("4. CHANGE OBFUSCATION") - print(" - Use CoinJoin or mixing services to break change detection") - print(" - Make change output and payment output similar in size") - print() - print("5. TIMING") - print(" - Broadcast at random times (not business hours)") - print(" - Use scheduled transactions if possible") - print() - print("6. FEE RATES") - print(" - Use non-standard fee rates (3.33, 4.47, 5.67)") - print(" - Avoid round numbers (1, 2, 5, 10 sat/byte)") - print() - print("7. INPUT SELECTION") - print(" - Minimize inputs (coin selection efficiency)") - print(" - Avoid using many small inputs unnecessarily") - print() - print("8. OUTPUT ORDERING") - print(" - Randomize output ordering") - print(" - Don't follow BIP69 deterministic ordering") - print() - - -if __name__ == "__main__": - main() diff --git a/privacy-track/resources/glossary.md b/privacy-track/resources/glossary.md index 6d3ac02..e8fb948 100644 --- a/privacy-track/resources/glossary.md +++ b/privacy-track/resources/glossary.md @@ -1,10 +1,10 @@ # Bitcoin Privacy Glossary -> Technical terms used throughout the Privacy Developer Track. Entries are ordered by when they first appear in the curriculum. +> Terms used in [The Privacy Sessions](../SESSIONS.md), grouped by the session where they first come up. --- -## Module 1: Chain Analysis +## S4-S5: Chain analysis, fingerprinting, coin selection **Common-Input-Ownership Heuristic (CIOH):** The assumption that all inputs in a Bitcoin transaction belong to the same entity. This is the most powerful heuristic used by chain analysis firms and is correct for the vast majority of normal transactions. Payjoin specifically exploits this assumption by having both sender and receiver contribute inputs. @@ -22,7 +22,7 @@ --- -## Module 2: Silent Payments +## S1-S3: Silent Payments **Silent Payments (BIP352):** A protocol that allows a receiver to publish a single static payment code from which senders can derive unique on-chain addresses for each payment. No interaction between sender and receiver is required. Each payment goes to a different address that only the receiver can detect. @@ -34,7 +34,13 @@ **Shared secret:** The point on the elliptic curve that both sender and receiver can independently compute via ECDH. In BIP352, this is derived from the sender's input keys and the receiver's scan key, then used to tweak the spend key to produce a unique output address. -**Tweak:** A scalar derived from the shared secret that is added to the receiver's spend key to produce the output public key. Different tweaks for different output indices allow multiple Silent Payment outputs in the same transaction. +**Tweak:** BIP352 uses the word in two places. (1) The output tweak `t_k`, a scalar derived from the shared secret and added to the receiver's spend key to produce output `k`. (2) The per-transaction *tweak data* `input_hash * A_sum`, which does not depend on any receiver's keys - so an indexer can precompute it for every transaction and serve it to light clients, who finish the ECDH with their own scan key. + +**K_max:** The BIP352 limit of 2323 outputs to the same scan key in one transaction. Senders must refuse to exceed it and scanners stop looking beyond it, which caps the work a malicious transaction can force on a receiver. + +**Eligible input:** An input whose public key BIP352 can read from the spending transaction (P2TR key path or non-NUMS script path, P2WPKH, P2SH-P2WPKH, P2PKH with a compressed key). Only eligible inputs are summed into `A_sum`; sender and receiver must agree exactly on this set. + +**Tweak indexer / index server:** A server that computes tweak data for every block so SP light clients don't have to fetch every transaction's prevouts. Examples: BlindBit Oracle, Shroud's indexer. A *remote scanner* (e.g. Frigate) instead receives the user's scan key and scans for them - faster, but the server sees incoming payments. **Tagged hash:** A domain-separated hash function defined in BIP340: `SHA256(SHA256(tag) || SHA256(tag) || data)`. Used throughout Silent Payments to prevent cross-protocol attacks. The tags "BIP0352/Inputs" and "BIP0352/SharedSecret" are used for input hashing and tweak derivation respectively. @@ -44,7 +50,7 @@ --- -## Module 3: Payjoin +## S6: Payjoin **Payjoin:** A collaborative transaction protocol where both sender and receiver contribute inputs. This breaks CIOH because a chain analyst cannot assume all inputs belong to the same entity. Defined in BIP78 (V1) and BIP77 (V2). @@ -64,11 +70,11 @@ --- -## Module 4: Wallet Privacy +## S5, S7, S9: Wallet privacy, light clients, CoinJoin and swaps **Coin selection:** The algorithm a wallet uses to choose which UTXOs to spend in a transaction. Different algorithms (largest-first, branch-and-bound, random) have different privacy and fee implications. -**Branch and bound:** A coin selection algorithm that searches for a combination of UTXOs whose total value exactly matches the target amount plus fees, eliminating the need for a change output. Used by Bitcoin Core as the preferred selection method. +**Branch and bound:** A coin selection algorithm that searches for a combination of UTXOs whose total value exactly matches the target amount plus fees, eliminating the need for a change output. Bitcoin Core runs several algorithms (BnB, CoinGrinder, Knapsack, Single Random Draw) and keeps the result with the lowest waste. **Waste metric:** A measure of the total cost of a coin selection result, including: fees paid in this transaction, cost of creating a change output, and future cost of spending the change. Minimizing waste optimizes for both fee efficiency and privacy. @@ -76,7 +82,7 @@ **Compact block filter (BIP158):** A probabilistic data structure that summarizes all the scriptPubKeys in a block. A light client downloads these small filters (~20KB each) and checks if any of their addresses might be in the block, without revealing which addresses they're watching to the serving node. -**Golomb-Rice coding:** The compression algorithm used in BIP158 filters. It efficiently encodes sorted lists of values where deltas between consecutive values follow a geometric distribution. The parameter P=19 gives a false positive rate of approximately 1/784,931. +**Golomb-Rice coding:** The compression algorithm used in BIP158 filters. It efficiently encodes sorted lists of values where deltas between consecutive values follow a geometric distribution. BIP158 uses Rice parameter P=19 with M=784,931, giving each queried item a false positive rate of about 1/784,931. **False positive:** When a compact block filter indicates a block might contain a relevant transaction when it actually doesn't. The client downloads the full block and discovers no match. BIP158's false positive rate is designed to be low enough that this wastes minimal bandwidth. @@ -92,9 +98,9 @@ **CoinSwap:** A privacy technique where two parties atomically swap UTXOs. Unlike CoinJoin, the two transactions look like normal payments on-chain - there is no recognizable multi-party pattern. Uses hash time-locked contracts (HTLCs) or adaptor signatures for atomic execution. -**Teleport Transactions:** Chris Belcher's implementation of CoinSwap for Bitcoin. Supports multi-hop swaps through intermediaries for stronger privacy. Currently in development, funded by OpenSats. +**Teleport Transactions / OpenSwap:** Chris Belcher's CoinSwap implementation (Teleport) continued as [openswap](https://github.com/citadel-foss/openswap) (formerly "coinswap"): a maker/taker market for multi-hop atomic swaps over Tor, with fidelity bonds against Sybils, moving to Taproot + MuSig2. -**JoinMarket NG:** The next generation of JoinMarket, a decentralized CoinJoin marketplace where "makers" provide liquidity (UTXOs for mixing) and "takers" pay a fee to mix their coins. Uses fidelity bonds to prevent Sybil attacks. +**JoinMarket:** A decentralized CoinJoin marketplace where "makers" provide liquidity (UTXOs for mixing) and "takers" pay a fee to mix their coins. Uses fidelity bonds to prevent Sybil attacks. **Fidelity bond:** Bitcoin locked in a time-locked output that proves a JoinMarket maker has "skin in the game." Larger and longer-locked bonds make Sybil attacks (where an adversary runs many maker nodes to de-anonymize takers) prohibitively expensive. @@ -106,7 +112,35 @@ **nSequence:** A per-input field originally intended for transaction replacement. Its value varies by wallet software, serving as another fingerprint. Modern wallets use 0xFFFFFFFD (signals RBF) or 0xFFFFFFFE (final, no RBF). -**Dust limit:** The minimum value a transaction output must have to be considered "standard" by Bitcoin Core's relay policy (currently 546 satoshis for P2WPKH). Outputs below this limit are uneconomical to spend and may reveal information about the wallet's coin selection algorithm. +**Dust limit:** The minimum value a transaction output must have to be considered "standard" by Bitcoin Core's relay policy (at the default dust relay fee: 546 sats for P2PKH, 294 for P2WPKH, 330 for P2TR). Outputs below this limit are uneconomical to spend and may reveal information about the wallet's coin selection algorithm. + +--- + +## S8: Network privacy + +**BIP324 (v2 P2P transport):** Encrypts connections between Bitcoin nodes. Stops passive observers (your ISP, a coffee-shop Wi-Fi) from reading or cheaply fingerprinting Bitcoin traffic. It does not hide what you send from the peer you send it to. + +**Private broadcast (`-privatebroadcast`):** Bitcoin Core 31 option: transactions submitted via `sendrawtransaction` (not yet wallet sends) are sent only via short-lived Tor or I2P connections, one transaction per connection, so recipients never learn your IP and can't link two of your transactions by connection. + +**ASmap:** A map from IP address to Autonomous System (the network operator). With `-asmap=1` (embedded data since Core 31, off by default) Core spreads peers across operators instead of IP ranges, making eclipse attacks by a single large hoster or ISP harder. + +**Eclipse attack:** Surrounding a node so every peer it talks to is controlled by the attacker, who can then hide blocks, watch its transactions, or feed it a fake view of the chain. + +**Dandelion++:** A proposed propagation scheme that relays a new transaction along a random single path before broadcasting it widely, hiding the origin. Not deployed in Bitcoin Core; private broadcast over Tor/I2P addresses the same leak differently. + +--- + +## S10-S11: Lightning and ecash + +**Probing:** Sending payments that are designed to fail in order to learn how much balance sits on each side of a channel. + +**Blinded path:** A Lightning route whose final hops are encrypted by the receiver, so the sender can pay without learning the receiver's node or channels. Used by BOLT12 offers. + +**BOLT12 offer:** A reusable Lightning payment request. Unlike a BOLT11 invoice it doesn't reveal the receiver's node ID and it supports blinded paths. + +**Blind signature:** A signature on a message the signer can't see. Ecash mints (Cashu, Fedimint) blind-sign tokens so they can't link a token's issuance to its redemption. + +**Cashu / Fedimint:** Chaumian ecash for Bitcoin. Cashu uses a single mint operator. Fedimint uses a federation of guardians with threshold custody. Both offer strong privacy from the operator in exchange for custodial trust. --- diff --git a/privacy-track/resources/reading-list.md b/privacy-track/resources/reading-list.md index 304ae3d..d917c1e 100644 --- a/privacy-track/resources/reading-list.md +++ b/privacy-track/resources/reading-list.md @@ -1,228 +1,74 @@ # Reading List -> Resources organized by phase covering all 24 sessions of the Code Orange Bitcoin Privacy Curriculum. Items marked **(R)** are required reading for that session. +> One short list per session of [The Privacy Sessions](../SESSIONS.md). **(R)** = read before the session if you can; everything else is for going deeper. Primary sources first, then explainers. Links verified September 2026. -## Phase 1: Foundations (Sessions 1-4) +## Always useful -### Bitcoin Privacy Fundamentals +- [Bitcoin Wiki: Privacy](https://en.bitcoin.it/wiki/Privacy): the long-form reference on how Bitcoin privacy breaks +- [Bitcoin Optech topics](https://bitcoinops.org/en/topics/): one page per protocol, with a history of every related change +- [Bitcoin Core PR Review Club](https://bitcoincore.reviews): the model for our Review Circle and S3 -- **(R)** [Bitcoin Wiki: Privacy](https://en.bitcoin.it/wiki/Privacy) - Comprehensive overview of privacy on Bitcoin -- **(R)** [Greg Maxwell CoinJoin proposal (2013)](https://bitcointalk.org/index.php?topic=279249.0) - Original CoinJoin design and rationale -- [Bitcoin Optech: Privacy Topic](https://bitcoinops.org/en/topics/privacy-terminology/) - Privacy terminology and concepts -- [Bitcoin Optech: Coin selection](https://bitcoinops.org/en/topics/coin-selection/) - Wallet algorithms affecting privacy -- [BIP 125: Opt-in Full Replace-by-Fee (RBF)](https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki) - Transaction replacement mechanics +## S1-S3 · Silent Payments -### Coin Selection & Wallet Fingerprinting +- **(R)** [BIP352: Silent Payments](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki), at least "Overview" and "Creating outputs" +- [BIP352 reference.py and test vectors](https://github.com/bitcoin/bips/tree/master/bip-0352): our [lab](../labs/silent-payments/) uses the same vectors +- [Optech: Silent Payments](https://bitcoinops.org/en/topics/silent-payments/) +- [BIP352 Index Server Specification](https://github.com/silent-payments/BIP0352-index-server-specification): tweak servers vs remote scanners (S2) +- [BIP375](https://github.com/bitcoin/bips/blob/master/bip-0375.mediawiki) + [BIP374](https://github.com/bitcoin/bips/blob/master/bip-0374.mediawiki): sending SP with PSBTs and DLEQ proofs (hardware wallets) +- Bitcoin Core: [tracking issue #28536](https://github.com/bitcoin/bitcoin/issues/28536), [#35301](https://github.com/bitcoin/bitcoin/pull/35301) (merged), [#35302](https://github.com/bitcoin/bitcoin/pull/35302), [#32966](https://github.com/bitcoin/bitcoin/pull/32966) (S3) -- **(R)** [Murch: "Evaluation of Coin Selection Strategies"](https://github.com/murchandamus/thesis) - Thesis on wallet fingerprinting via UTXO selection -- **(R)** [0xB10C: Wallet fingerprinting via change output analysis](https://b10c.me/blog/) - Change pattern detection techniques -- [BIP 69: Lexicographic Indexing of Transaction Inputs and Outputs](https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki) - Deterministic ordering to prevent fingerprinting -- [Murch: UTXO selection algorithms](https://github.com/murchandamus/bitscripts/tree/master/coinselection) - Reference implementations +## S4 · Chain analysis and fingerprinting -### UTXO Model & Privacy +- **(R)** [Bitcoin Wiki: Blockchain attacks on privacy](https://en.bitcoin.it/wiki/Privacy#Blockchain_attacks_on_privacy) +- [Ishaana Misra: Wallet Fingerprinting](https://ishaana.com/blog/wallet_fingerprinting/): real wallets, real tells +- Hughes, [A Cypherpunk's Manifesto](https://www.activism.net/cypherpunk/manifesto.html) (1993) -- [Jameson Lopp: "The Ultimate Guide to Bitcoin"](https://www.lopp.net/bitcoin-information.html) - Privacy section covers UTXO model -- [Bitcoin Core reference: Output script types](https://developer.bitcoin.org/reference/transactions.html) - Script types and their identifiability +## S5 · Coin selection and change -## Phase 2: Silent Payments (Sessions 5-8) +- **(R)** [Optech: Coin selection](https://bitcoinops.org/en/topics/coin-selection/) +- Erhardt, [An Evaluation of Coin Selection Strategies](https://murch.one/erhardt2016coinselection.pdf) (2016) +- [BIP329: Wallet label export](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki) · [Optech: Wallet labels](https://bitcoinops.org/en/topics/wallet-labels/) -### BIP 352 Silent Payments Specification +## S6 · Payjoin -- **(R)** [BIP 352: Silent Payments](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) - Full specification with test vectors -- [BIP 352 GitHub Discussion](https://github.com/bitcoin/bips/issues/1458) - Design rationale and alternatives considered -- [Josie Baker: "Light client support for Silent Payments"](https://github.com/josibake/bitcoin-articles/blob/main/silent_payments_light_client_support.md) - Privacy in SPV clients +- **(R)** [payjoin.org](https://payjoin.org): the why, plus who supports it +- [BIP77: Async Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0077.md) · [BIP78: Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki) +- [Optech: Payjoin](https://bitcoinops.org/en/topics/payjoin/) -### Silent Payments Research & Design +## S7 · Light clients and your own node -- **(R)** [Ruben Somsen: Silent Payments gist (original design)](https://gist.github.com/RubenSomsen/c43b79517e7cb701ebf77ebc6231d35e) - Original proposal and discussion -- [Silent Payments: Light Client Protocol](https://github.com/bitcoindevkit/bdk/discussions/941) - Client implementation considerations -- [Kixunil: Silent Payments analysis](https://github.com/Kixunil/bips/blob/silent_payments_analysis/bip_0352_analysis.md) - Security analysis +- **(R)** [Optech: Compact block filters](https://bitcoinops.org/en/topics/compact-block-filters/) +- [BIP157](https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki) · [BIP158](https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki) +- [Optech: Utreexo](https://bitcoinops.org/en/topics/utreexo/) · [Floresta](https://github.com/getfloresta/Floresta) · [Kyoto](https://github.com/2140-dev/kyoto) +- Satoshi, [Bitcoin whitepaper](https://bitcoin.org/bitcoin.pdf), §8 (SPV) and §10 (Privacy) -### Implementation & Libraries +## S8 · Network privacy -- [BDK Silent Payments module](https://github.com/bitcoindevkit/bdk/tree/master/crates/bdk_wallet) - Rust implementation -- [secp256k1 library](https://github.com/bitcoin-core/secp256k1) - Underlying elliptic curve math -- [Rust Secp256k1 bindings](https://docs.rs/secp256k1/latest/secp256k1/) - API documentation -- [Silent Payments test vectors](https://github.com/bitcoin/bips/blob/master/bip-0352/test-vectors.json) - Interoperability testing +- **(R)** [Bitcoin Core 31.0 release notes](https://bitcoincore.org/en/releases/31.0/): `-privatebroadcast` and embedded asmap +- [BIP324: v2 P2P transport](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki) · [Optech: v2 transport](https://bitcoinops.org/en/topics/v2-p2p-transport/) +- [Optech: Eclipse attacks](https://bitcoinops.org/en/topics/eclipse-attacks/) +- Biryukov, Khovratovich, Pustogarov, [Deanonymisation of clients in Bitcoin P2P network](https://arxiv.org/abs/1405.7418) (2014) +- May, [The Crypto Anarchist Manifesto](https://www.activism.net/cypherpunk/crypto-anarchy.html) (1988) -## Phase 3: Payjoin (Sessions 9-12) +## S9 · CoinJoin and swaps -### Payjoin Specification (BIP 77/78) +- **(R)** Maxwell, [CoinJoin: Bitcoin privacy for the real world](https://bitcointalk.org/index.php?topic=279249.0) (2013) +- [Optech: CoinJoin](https://bitcoinops.org/en/topics/coinjoin/) · [Optech: CoinSwap](https://bitcoinops.org/en/topics/coinswap/) +- Belcher, [Design for a CoinSwap implementation](https://gist.github.com/chris-belcher/9144bd57a91c194e332fb5ca371d0964) +- [JoinMarket](https://github.com/JoinMarket-Org/joinmarket-clientserver) · [openswap](https://github.com/citadel-foss/openswap) -- **(R)** [BIP 78: Payjoin (Pay-to-contract Protocol)](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki) - Full specification -- **(R)** [BIP 77: Payjoin URI scheme](https://github.com/bitcoin/bips/blob/master/bip-0077.mediawiki) - URI format for initiating payjoin -- [Payjoin Specification](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#specification) - Detailed protocol flow +## S10 · Lightning privacy -### Payjoin Research & Design +- **(R)** [Optech: Offers (BOLT12)](https://bitcoinops.org/en/topics/offers/) +- [BOLT12 spec](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md) +- [Polar](https://lightningpolar.com): one-click regtest Lightning networks for the hands-on -- **(R)** [Dan Gould: "Payjoin is Practical"](https://gist.github.com/dgould/e9bab83f8a89de22babe3e31ef937eee) - Privacy analysis and practical considerations -- [Stonehedge Labs: Payjoin privacy](https://medium.com/coinmonks/payjoin-privacy-analysis-c9e7e7d9a4c0) - Real-world privacy evaluation -- [Dan Gould: Payjoin for PaymentPointers](https://github.com/royalpay/payjoin-paymentpointers) - Extended payjoin protocols +## S11 · Ecash -### Implementation & Tools - -- [Payjoin Dev Kit (PDK)](https://github.com/payjoin/payjoin) - Reference Rust implementation -- [Payjoin specification reference](https://payjoin.org/) - Payjoin.org resource hub -- [Bluewallet Payjoin support](https://bluewallet.io/payjoin/) - Mobile wallet implementation -- [Sparrow Wallet Payjoin](https://www.sparrowwallet.com/) - Desktop wallet with Payjoin -- [Zeus Payjoin module](https://github.com/ZeusLN/zeus) - Lightning wallet with Payjoin - -## Phase 4: Network & Protocol Privacy (Sessions 13-16) - -### Dandelion Protocol (BIP 156) - -- **(R)** [BIP 156: Dandelion++ (Anonymity in the Bitcoin P2P Network)](https://github.com/bitcoin/bips/blob/master/bip-0156.mediawiki) - Full specification -- [Dandelion++ paper](https://arxiv.org/abs/1805.11060) - Academic research on P2P anonymity -- [Bitcoin Core Dandelion++ PR discussions](https://github.com/bitcoin/bitcoin/pulls?q=dandelion) - Implementation discussion - -### Compact Block Filters (BIP 157/158) - -- **(R)** [BIP 157: Client Side Block Filtering](https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki) - Compact block filter specification -- **(R)** [BIP 158: Compact Block Filters for Segregated Witness](https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki) - Extension for SegWit -- [Oxt.me: BIP 157 analysis](https://oxt.me/blog/) - Privacy analysis of block filters -- [Bitcoin Core BIP 157 PR](https://github.com/bitcoin/bitcoin/pull/18876) - Implementation notes - -### Light Client & SPV Privacy - -- [Floresta: Stateless Bitcoin validation](https://github.com/bitcoin-dev-kit/floresta) - Light client library -- [Kyoto: BIP 324 transport layer](https://github.com/lexe-app/kyoto) - Privacy-focused light client -- [Electrum: SPV implementation](https://electrum.org/) - Traditional SPV wallet -- [BDK Light Client](https://github.com/bitcoindevkit/bdk/tree/master/crates/bdk_sqlite) - BDK light client support - -### Taproot & Tapscript (BIP 340/341/342) - -- **(R)** [BIP 340: Schnorr Signatures for secp256k1](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) - Schnorr signature specification -- **(R)** [BIP 341: Taproot: SegWit version 1 spending rules](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) - Taproot specification -- **(R)** [BIP 342: Validation of Taproot Scripts](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki) - Script validation rules -- [Taproot privacy benefits](https://bitcoinops.org/en/topics/taproot/) - Optech Taproot topic - -### Script Privacy & CISA - -- [Cross-Input Signature Aggregation (CISA) research](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2024-February/) - Proposed enhancement for privacy -- [CISA design document](https://github.com/sipa/bips/blob/bip-cross-input-aggregation/bip-cross-input-aggregation.mediawiki) - Specification draft - -## Phase 5: Advanced (Sessions 17-20) - -### CoinJoin & Mixing - -- **(R)** [Greg Maxwell: CoinJoin proposal (2013)](https://bitcointalk.org/index.php?topic=279249.0) - Original design -- [WabiSabi: Mixing protocol paper](https://arxiv.org/abs/2106.04253) - Privacy-preserving mixing -- [Whirlpool: Samourai implementation](https://samouraiwallet.com/whirlpool) - Practical mixing service -- [WabiSabi GitHub](https://github.com/zkSNACKs/WabiSabi) - Reference implementation - -### Teleport Transactions - -- **(R)** [Teleport Transactions: design document](https://github.com/RubenSomsen/teleport-transactions) - Design and motivation -- [Ruben Somsen: Teleport Transactions blog](https://ruben.somsen.com/2020/04/20/teleport-transactions/) - Explanation and privacy analysis - -### eCash & Blind Signatures - -- **(R)** [David Chaum: "Blind Signatures for Untraceable Payments" (1983)](https://chaum.com/publications/Chaum-blind-signatures.PDF) - Foundational paper -- [Cashu: eCash protocol](https://github.com/cashubtc/cashu) - Bitcoin-backed eCash implementation -- [Cashu NUT (Notation, Usage, and Test vectors)](https://github.com/cashubtc/nuts) - Cashu specification -- [Fedimint: Federated eCash](https://fedimint.org/) - Threshold-signature eCash -- [Fedimint whitepaper](https://docs.fedimint.org/) - Architecture and security - -### Lightning Network Privacy - -- **(R)** [BOLT 4: Onion Routing](https://github.com/lightning/bolts/blob/master/04-onion-routing.md) - Encrypted routing specification -- [BOLT 11: Invoice Protocol](https://github.com/lightning/bolts/blob/master/11-payment-encoding.md) - Invoice format -- [BOLT 12: Offers](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md) - Upgraded offer protocol -- [Rusty Russell: Lightning privacy analysis](https://rusty.ozlabs.org/) - Expert analysis -- [LND Privacy considerations](https://github.com/lightningnetwork/lnd/blob/master/docs/SAFETY.md) - Node implementation notes - -## Phase 6: Building & Contributing (Sessions 21-24) - -### Bitcoin Development Kit (BDK) - -- **(R)** [BDK Documentation](https://docs.rs/bdk/latest/bdk/) - Complete API reference -- [BDK GitHub](https://github.com/bitcoindevkit/bdk) - Source code -- [BDK tutorial: Building a wallet](https://github.com/bitcoindevkit/bdk/tree/master/examples) - Example code -- [BDK discussions](https://github.com/bitcoindevkit/bdk/discussions) - Community Q&A - -### Bitcoin Core & Network - -- [Bitcoin Core GitHub](https://github.com/bitcoin/bitcoin) - Main repository -- [Bitcoin Core contributing guide](https://github.com/bitcoin/bitcoin/blob/master/CONTRIBUTING.md) - Contribution process -- [Bitcoin Core P2P documentation](https://developer.bitcoin.org/reference/p2p_networking.html) - Network protocol -- [Bitcoin Core RPC reference](https://developer.bitcoin.org/reference/rpc/) - API documentation - -### Research & Analysis Tools - -- **(R)** [OXT Research: Blockchain analysis methodology](https://oxt.me/) - Analysis frameworks and tools -- [Glassnode: On-chain analytics](https://glassnode.com/) - Data and research -- [Chainalysis: Transaction analysis](https://www.chainalysis.com/) - Commercial tool (for defensive knowledge) -- [Elliptic: Privacy risk assessment](https://www.elliptic.co/) - Enterprise tool -- [CoinJoin tracker tools](https://github.com/nopara73/CoinJoinWatch) - Monitoring and detection - -### Privacy Tool Development - -- [Wasabi Wallet](https://www.wasabiwallet.io/) - Privacy-focused desktop wallet -- [Samourai Wallet](https://samouraiwallet.com/) - Mobile privacy wallet -- [BTCPay Server](https://btcpayserver.org/) - Self-hosted payment processor -- [Electrum Server (ElectrumX)](https://electrumx-spesmilo.readthedocs.io/) - SPV server implementation - -## Reference Materials - -### Cryptographic Foundations - -- [Elliptic Curve Cryptography (ECC) introduction](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) - Mathematical foundations -- [secp256k1 curve specification](https://en.wikipedia.org/wiki/Secp256k1) - Bitcoin's curve -- [Schnorr signature scheme](https://en.wikipedia.org/wiki/Schnorr_signature) - Signature scheme background -- [Hash functions in Bitcoin](https://en.bitcoin.it/wiki/Hash_functions) - SHA-256, RIPEMD-160, etc. - -### Bitcoin Protocol - -- [Bitcoin: A Peer-to-Peer Electronic Cash System (Whitepaper)](https://bitcoin.org/bitcoin.pdf) - Original design -- [Bitcoin Script reference](https://en.bitcoin.it/wiki/Script) - Scripting language documentation -- [Bitcoin Transactions](https://developer.bitcoin.org/reference/transactions.html) - Transaction structure -- [Bitcoin Blocks](https://developer.bitcoin.org/reference/block_chain.html) - Block structure and consensus - -### Standards & RFCs - -- [BIP Index](https://github.com/bitcoin/bips) - All Bitcoin Improvement Proposals -- [BOLT Specification](https://github.com/lightning/bolts) - Lightning Network standards -- [RFC 2104: HMAC](https://tools.ietf.org/html/rfc2104) - Keyed hashing -- [RFC 3394: AES Key Wrap](https://tools.ietf.org/html/rfc3394) - Key encryption - -### Learning Resources - -- [Mastering Bitcoin by Andreas M. Antonopoulos](https://github.com/bitcoinbook/bitcoinbook) - Technical introduction (free online) -- [Programming Bitcoin by Jimmy Song](https://jimmysong.io/programmingbitcoin/) - Practical coding guide -- [Learning Bitcoin from the Command Line](https://github.com/BlockchainCommons/Learning-Bitcoin-from-the-Command-Line) - Hands-on tutorial -- [Bitcoin Optech](https://bitcoinops.org/) - Technical resources and education - -## Session-to-Resource Mapping - -| Session | Topic | Key Resources | -|---------|-------|---| -| 1 | Privacy Foundations | Bitcoin Wiki Privacy, Maxwell CoinJoin | -| 2 | UTXO Model | Optech Coin Selection, Lopp guide | -| 3 | Change Analysis | Murch thesis, 0xB10C fingerprinting | -| 4 | Wallet Fingerprinting | BIP 69, wallet implementations | -| 5 | Silent Payments Intro | BIP 352, Somsen gist | -| 6 | SP Key Derivation | BIP 352 spec, secp256k1 docs | -| 7 | SP Light Clients | Josie Baker protocol, BDK | -| 8 | SP in Practice | ENuts, implementation guides | -| 9 | Payjoin Protocol | BIP 77/78, Gould analysis | -| 10 | Payjoin Implementation | PDK, Bluewallet, Sparrow | -| 11 | Receiver Privacy | BIP 78 spec, Gould practical guide | -| 12 | Payjoin + SP | Combined protocols, design docs | -| 13 | P2P Anonymity | BIP 156, Dandelion++ paper | -| 14 | Light Client Privacy | BIP 157/158, Floresta, Kyoto | -| 15 | Transaction Relay | Bitcoin Core P2P, protocol docs | -| 16 | Taproot Privacy | BIP 340/341/342, Optech | -| 17 | CoinJoin Basics | Maxwell proposal, WabiSabi paper | -| 18 | Teleport Txs | Somsen design doc, analysis | -| 19 | eCash & Blind Sigs | Chaum 1983, Cashu/Fedimint docs | -| 20 | Lightning Privacy | BOLT 4, Russell analysis | -| 21 | Wallet Development | BDK docs, examples | -| 22 | Privacy Analysis | OXT methodology, analysis tools | -| 23 | Bitcoin Core Contrib | Contributing guide, source code | -| 24 | Capstone Project | All previous resources | +- **(R)** [Optech: Ecash](https://bitcoinops.org/en/topics/ecash/) +- [cashu.space](https://cashu.space) · [fedimint.org](https://fedimint.org) +- Chaum, *Security without Identification: Transaction Systems to Make Big Brother Obsolete* (Communications of the ACM, 1985) --- -**Last Updated:** May 2026 - -**Note:** This reading list is maintained as a living document. Resources are curated for accuracy and relevance to Bitcoin privacy. Links may change; GitHub repositories and academic papers are most stable sources. +Found a better source, or a dead link? PRs welcome. See [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/tools/check_links.py b/tools/check_links.py new file mode 100644 index 0000000..ed64e36 --- /dev/null +++ b/tools/check_links.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Fail if any relative markdown link in the repo points at a missing file.""" +import re +import sys +from pathlib import Path + +root = Path(__file__).resolve().parent.parent +bad = [] +for md in root.rglob("*.md"): + if ".git" in md.parts: + continue + for target in re.findall(r"\]\(([^)\s]+)\)", md.read_text(encoding="utf-8")): + if target.startswith(("http://", "https://", "mailto:", "#")): + continue + path = target.split("#")[0] + if path and not (md.parent / path).exists(): + bad.append(f"{md.relative_to(root)}: {target}") +print("\n".join(bad) or "all relative links resolve") +sys.exit(1 if bad else 0)