Skip to content

illustrate the decay regimes at 32 wallets - #39

Open
bc1cindy wants to merge 1 commit into
problem-definitionfrom
ctp-figure-32
Open

bc1cindy wants to merge 1 commit into
problem-definitionfrom
ctp-figure-32

Conversation

@bc1cindy

@bc1cindy bc1cindy commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

superseeded #38

adds 32 wallet visualization of the decay, shipped as light and dark external svg files that the book paints per theme

the sources run along the rows and the rounds along the columns, with the same coin struck in both graphs

follow up

Captura de Tela 2026-09-24 às 22 39 25 Captura de Tela 2026-09-24 às 22 39 43 Captura de Tela 2026-09-24 às 22 44 18 Captura de Tela 2026-09-24 às 22 45 44 Captura de Tela 2026-09-24 às 22 45 58

besides that, there are the open title suggestions that @yuval-block asked for:

so to be a bit more precise i think the "collaborative transaction privacy" title is descriptive of the content, but it doesn't quite capture that in the context of this repo it defines the problem that we're trying to solve

and if we just emphasize problem definition for fungi, that sells short the applicability of this perspective to alternatives

so i think a good title would address both of these, whereas my current title and your proposed alternatives only focuses on the first function of this doc

suggestions:

  • The deanonymization problem in collaborative transactions
  • What collaborative transactions must not leak

any other?

Comment thread theme/diagrams.css Resolved
Comment thread theme/diagrams.css Resolved
@bc1cindy
bc1cindy force-pushed the ctp-figure-32 branch 2 times, most recently from 9c81510 to 897ff1b Compare September 27, 2026 15:35
@oakagent

oakagent commented Oct 1, 2026

Copy link
Copy Markdown

ACK 897ff1b (automated): reviewed 1 commit 897ff1b; no findings.

@oakagent

oakagent commented Oct 8, 2026

Copy link
Copy Markdown

ACK dc3faa7 (automated): reviewed 1 commit dc3faa7; no findings.

@yuval-block

yuval-block commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

in the variant that eventually decided to go with the crossbar style connections also straddle the original input coins, which is a bit confusing, the n=16 diagram doesn't have this glitch
image

also using the colors that were used for alice and bob is perhaps confusing i think it would be clearer if the candidates were in the user's color but faded, struck off items displayayed as neutral and confirmed origin displayed in the user color

@bc1cindy

bc1cindy commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

in the variant that eventually decided to go with the crossbar style connections also straddle the original input coins, which is a bit confusing, the n=16 diagram doesn't have this glitch image

also using the colors that were used for alice and bob is perhaps confusing i think it would be clearer if the candidates were in the user's color but faded, struck off items displayayed as neutral and confirmed origin displayed in the user color

  • crossbars now sit in the gap between rounds instead of over the coins
  • candidates are a faded version of alice's color, struck off coins are neutral gray, and the confirmed origin is alice's full color (bob's orange is gone)
  • lines leading into struck off coins are gray too
  • dropped the extra 8th column: the fragile graph only has 6 joins, so alice's output was left unconnected.
Captura de Tela 2026-10-09 às 18 44 01 Captura de Tela 2026-10-09 às 18 44 17 Captura de Tela 2026-10-09 às 18 44 39 Captura de Tela 2026-10-09 às 18 44 56

what about now? @yuval-block

@yuval-block

Copy link
Copy Markdown
Collaborator

yeah i think this is clearer, thank you

@oakagent

Copy link
Copy Markdown

ACK e647534 (automated): reviewed 1 commit e647534; no findings.

@bc1cindy bc1cindy mentioned this pull request Oct 15, 2026
Comment thread contrib/figures.py Resolved
Comment thread theme/diagrams.css Resolved
@bc1cindy bc1cindy added documentation Improvements or additions to documentation high labels Oct 20, 2026
@bc1cindy bc1cindy removed high documentation Improvements or additions to documentation labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
range-diff for force push e647534178 → 43d4932e96 October 21, 2026 21:37 UTC: 1 modified

reproduce: git range-diff 6149b07..e647534 6149b07..43d4932

1:  e647534178 ! 1:  43d4932e96 illustrate the decay regimes at 32 wallets
1: e647534178 ! 1: 43d4932e96 illustrate the decay regimes at 32 wallets
@@ collaborative_txn_privacy.md: For example, a chain analysis vendor with KYC info
  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.">
++<div class="chart-src f32">
 +<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.">
@@ contrib/figures.py (new)
 +import json
 +import re
 +import sys
++from html import escape
 +from html.parser import HTMLParser
 +from pathlib import Path
 +
@@ contrib/figures.py (new)
 +    def __init__(self, markup):
 +        super().__init__()
 +        self.images = {}
++        self.alt = ''
 +        self.feed(markup)
 +
 +    def handle_starttag(self, tag, attrs):
 +        attrs = dict(attrs)
 +        if tag == 'img':
 +            self.images['light'] = attrs.get('src', '')
++            self.alt = attrs.get('alt', '')
 +        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
++    picture = Picture(match.group())
++    images = picture.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)
@@ contrib/figures.py (new)
 +            svg,
 +            flags=re.S,
 +        )
++        # The svg replaces the img, so it has to carry the img's alt as its label.
++        if picture.alt:
++            svg = re.sub(
++                r'<svg\b[^>]*>',
++                lambda tag: re.sub(r'\s(?:role|aria-label)="[^"]*"', '', tag.group())[:-1]
++                + f' role="img" aria-label="{escape(picture.alt)}">',
++                svg,
++                count=1,
++            )
 +        # 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())
@@ src/figures/figure-32-light.svg (new)
  \ No newline at end of file
 
  ## theme/diagrams.css ##
+@@
+    once per theme and swapped here; graphviz keeps whatever its .dot source
+    asks for, with only graphviz's own defaults lifted for the dark themes. */
+ .mdbook-graphviz-output,
+-.mermaid {
++.mermaid,
++.chart-src {
+     overflow-x: auto;
+     margin: 1rem 0;
+ }
+@@
+ /* Scale wide graphs down to the content column instead of clipping them;
+    the svg viewBox keeps them sharp at any size. */
+ .mdbook-graphviz-output svg,
+-.mermaid svg {
++.mermaid svg,
++.chart-src svg {
+     display: block;
+     max-width: 100%;
+     height: auto;
+@@
+    only just overflow still fit without scrolling. */
+ @media (max-width: 700px) {
+     .mdbook-graphviz-output,
+-    .mermaid {
++    .mermaid,
++    .chart-src {
+         margin-left: calc(-1 * var(--page-padding, 15px));
+         margin-right: calc(-1 * var(--page-padding, 15px));
+         padding: 0 var(--page-padding, 15px);
+     }
+ 
+     .mdbook-graphviz-output svg,
+-    .mermaid svg {
++    .mermaid svg,
++    .chart-src svg {
+         max-width: none;
+     }
+ }
 @@ theme/diagrams.css: html:is(.coal, .navy, .ayu) .mdbook-graphviz-output [fill="black"] {
  html:is(.coal, .navy, .ayu) .mdbook-graphviz-output text:not([fill]) {
      fill: #b7b7b7;
@@ theme/diagrams.css: html:is(.coal, .navy, .ayu) .mdbook-graphviz-output [fill="b
 +
 +/* The build embeds both palettes as inline SVG. GitHub keeps the source
 +   picture; book themes select a palette without fetching another image. */
-+.chart-src {
-+    margin: 1rem auto;
-+}
-+
-+.chart-src svg {
-+    display: block;
-+    max-width: 100%;
-+    height: auto;
-+}
-+
 +.chart-dark {
 +    display: none;
 +}

Comment thread contrib/figures.py Resolved
Comment thread contrib/figures.py Resolved
Ten strikes leave twenty-two candidates against one.

Keep light and dark SVGs as separate files, marked with GitHub's
#gh-light-mode-only and #gh-dark-mode-only fragments. The book selects
the same files by theme, and prints the light one.
@github-actions

Copy link
Copy Markdown
range-diff for force push 43d4932e96 → ac4a500b6a October 22, 2026 01:41 UTC: 1 modified

reproduce: git range-diff 6149b07..43d4932 6149b07..ac4a500

1:  43d4932e96 ! 1:  ac4a500b6a illustrate the decay regimes at 32 wallets
1: 43d4932e96 ! 1: ac4a500b6a illustrate the decay regimes at 32 wallets
@@ Commit message
 
     Ten strikes leave twenty-two candidates against one.
 
-    Keep light and dark SVGs as separate files, referenced through a picture
-    in the Markdown source so the figure renders on GitHub.
-
-    During the mdBook build only, embed both SVGs into the generated HTML.
-    Select the version matching the book theme and use the light version
-    for printing, avoiding extra image requests and CSS backgrounds.
+    Keep light and dark SVGs as separate files, marked with GitHub's
+    #gh-light-mode-only and #gh-dark-mode-only fragments. The book selects
+    the same files by theme, and prints the light one.
 
  ## book.toml ##
 @@ book.toml: after = ["links", "graphviz"]
@@ collaborative_txn_privacy.md: For example, a chain analysis vendor with KYC info
  
  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">
-+<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>
++<img src="src/figures/figure-32-light.svg#gh-light-mode-only" loading="lazy" 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.">
++<img src="src/figures/figure-32-dark.svg#gh-dark-mode-only" loading="lazy" 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.">
 +</div>
 +
 +*The same ten peers, deanonymized in the same order, in both graphs.*
@@ collaborative_txn_privacy.md: For example, a chain analysis vendor with KYC info
  ## contrib/figures.py (new) ##
 @@
 +#!/usr/bin/env python3
-+"""Embed chart pictures in the book; keep repository image paths on GitHub."""
++"""Point chart images at the book's copy; the source keeps repository paths for GitHub."""
 +import json
-+import re
 +import sys
-+from html import escape
-+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.alt = ''
-+        self.feed(markup)
-+
-+    def handle_starttag(self, tag, attrs):
-+        attrs = dict(attrs)
-+        if tag == 'img':
-+            self.images['light'] = attrs.get('src', '')
-+            self.alt = attrs.get('alt', '')
-+        elif tag == 'source' and attrs.get('media') == '(prefers-color-scheme: dark)':
-+            self.images['dark'] = attrs.get('srcset', '')
-+
-+
-+def embed(match, root):
-+    picture = Picture(match.group())
-+    images = picture.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,
-+        )
-+        # The svg replaces the img, so it has to carry the img's alt as its label.
-+        if picture.alt:
-+            svg = re.sub(
-+                r'<svg\b[^>]*>',
-+                lambda tag: re.sub(r'\s(?:role|aria-label)="[^"]*"', '', tag.group())[:-1]
-+                + f' role="img" aria-label="{escape(picture.alt)}">',
-+                svg,
-+                count=1,
-+            )
-+        # 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):
++def walk(items):
 +    for item in items:
-+        chapter = item.get('Chapter')
++        chapter = item.get('Chapter') if isinstance(item, dict) else None
 +        if chapter:
-+            chapter['content'] = PICTURE.sub(lambda match: embed(match, root), chapter['content'])
-+            walk(chapter['sub_items'], root)
++            chapter['content'] = chapter['content'].replace('src="src/figures/', 'src="figures/')
++            walk(chapter['sub_items'])
 +
 +
 +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']))
++    walk(book.get('items', book.get('sections', [])))
 +    json.dump(book, sys.stdout)
 +
 +
@@ theme/diagrams.css
  .mdbook-graphviz-output svg,
 -.mermaid svg {
 +.mermaid svg,
-+.chart-src svg {
++.chart-src img {
      display: block;
      max-width: 100%;
      height: auto;
@@ theme/diagrams.css
      .mdbook-graphviz-output svg,
 -    .mermaid svg {
 +    .mermaid svg,
-+    .chart-src svg {
++    .chart-src img {
          max-width: none;
      }
  }
@@ theme/diagrams.css: html:is(.coal, .navy, .ayu) .mdbook-graphviz-output [fill="b
      fill: #b7b7b7;
  }
 +
-+/* The build embeds both palettes as inline SVG. GitHub keeps the source
-+   picture; book themes select a palette without fetching another image. */
-+.chart-dark {
++/* Each chart has a light and a dark file, marked for GitHub by its URL
++   fragment; book themes select one the same way. */
++.chart-src img[src$="#gh-dark-mode-only"] {
 +    display: none;
 +}
 +
 +@media screen {
-+    html:is(.coal, .navy, .ayu) .chart-dark {
++    html:is(.coal, .navy, .ayu) .chart-src img[src$="#gh-dark-mode-only"] {
 +        display: block;
 +    }
 +
-+    html:is(.coal, .navy, .ayu) .chart-light {
++    html:is(.coal, .navy, .ayu) .chart-src img[src$="#gh-light-mode-only"] {
 +        display: none;
 +    }
 +}

@oakagent

Copy link
Copy Markdown

ACK ac4a500

oak · automated · ac4a500 · 1 commit · 0 findings

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants