Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ after = ["links", "graphviz"]
command = "mdbook-katex"
after = ["links"]

[preprocessor.figures]
command = "python3 contrib/figures.py"
after = ["links", "graphviz", "mermaid", "katex"]

[output.html]
additional-css = ["theme/diagrams.css"]
git-repository-url = "https://github.com/fungi-protocol/docs"
8 changes: 8 additions & 0 deletions collaborative_txn_privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,14 @@ For example, a chain analysis vendor with KYC information may already be able to
If the CoinJoin graph is *robustly connected*[^flow], then no small cut separates any output from the mass of its candidate origin coins. Stated differently, every output is connected to its candidate origins by multiple disjoint paths.

This redundancy forestalls the cliff of exponential decay, where privacy becomes much more brittle, extending the duration of the linear decay regime by requiring the adversary to deanonymize a much larger proportion of users before divide-and-conquer tactics start coming into play.
<div class="chart-src f32" role="img" aria-label="Two CoinJoin graphs of thirty-two wallets under the same ten deanonymizations.">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="src/figures/figure-32-dark.svg">
<img src="src/figures/figure-32-light.svg" alt="Two CoinJoin graphs of thirty-two wallets under the same ten deanonymizations: mixing sheds one candidate per strike; merges through a single coin collapse to one.">
</picture>
</div>

*The same ten peers, deanonymized in the same order, in both graphs.*

But when two post-CoinJoin outputs are linked, on-chain or otherwise, this makes **intersection attacks**[^goldfeder][^scroll-intersection] possible. In such attacks, we take the candidate origins of these now-linked coins and check for any overlaps.

Expand Down
74 changes: 74 additions & 0 deletions contrib/figures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Embed chart pictures in the book; keep repository image paths on GitHub."""
import json
import re
import sys
from html.parser import HTMLParser
from pathlib import Path

PICTURE = re.compile(r'<picture>\s*<source\b[^>]*>\s*<img\b[^>]*>\s*</picture>', re.S)


class Picture(HTMLParser):
def __init__(self, markup):
super().__init__()
self.images = {}
self.feed(markup)

def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == 'img':
self.images['light'] = attrs.get('src', '')
elif tag == 'source' and attrs.get('media') == '(prefers-color-scheme: dark)':
self.images['dark'] = attrs.get('srcset', '')


def embed(match, root):
images = Picture(match.group()).images
# Only handle the paired, checked-in chart assets, not unrelated pictures.
if set(images) != {'light', 'dark'} or not all(
re.fullmatch(r'src/figures/figure-\d+-' + theme + r'\.svg', path)
for theme, path in images.items()
):
return match.group()
fragments = []
for theme in ('light', 'dark'):
svg = (root / images[theme]).read_text()
scope = 'chart-' + Path(images[theme]).stem
# Inline SVG styles belong to the whole document. Scope the asset's
# class selectors so its palette cannot affect other figures/diagrams.
svg = re.sub(
r'<style>(.*?)</style>',
lambda style: '<style>' + re.sub(
r'(\.[a-zA-Z_][\w-]*)\s*\{',
lambda selector: '.' + scope + ' ' + selector.group(1) + '{',
style.group(1),
) + '</style>',
svg,
flags=re.S,
)
# A blank line ends a CommonMark raw HTML block. Keep the SVG in
# one block so its text and shapes are not parsed as Markdown.
svg = '\n'.join(line for line in svg.splitlines() if line.strip())
fragments.append(f'<div class="chart-{theme} {scope}">\n{svg}\n</div>')
return '\n'.join(fragments)


def walk(items, root):
for item in items:
chapter = item.get('Chapter')
if chapter:
chapter['content'] = PICTURE.sub(lambda match: embed(match, root), chapter['content'])
walk(chapter['sub_items'], root)


def main():
if len(sys.argv) > 1 and sys.argv[1] == 'supports':
sys.exit(0 if len(sys.argv) > 2 and sys.argv[2] == 'html' else 1)
context, book = json.load(sys.stdin)
walk(book.get('items', book.get('sections', [])), Path(context['root']))
json.dump(book, sys.stdout)


if __name__ == '__main__':
main()
Loading
Loading