# Decision log

Newest first. Each entry: what was decided, by whom, why, and what it invalidates.

---

## D-002 — Target chain changes from Litecoin to Pepecoin (2026-09-10)

**Decided by:** Martin.
> "Cancel the light coin install. Let's work on Pepe coin but not inscriptions like Doge's.
> Inscriptions like using Omni. I think there are better features with Omni like the check sum
> validator and built-in token exchange"

**Action taken:** Litecoin Core install cancelled and fully removed (binaries, datadir, staged
tarballs). Nothing was left on disk; 416 GiB free, same as before we started.

**What this invalidates:**
- Phase 2 as originally written (recovering Martin's 2021-2023 Omni inscriptions from Litecoin)
  is **suspended, not impossible**. See "Recovery is not lost" below.
- Phase 3's plan to validate our consensus engine against a live OmniLite reference node.

**What survives unchanged:** Phase 1 Omni research. The Omni protocol sources (OmniLite,
Omni Core, the spec) remain the ground truth regardless of which chain we deploy on.

---

## D-001 — Proceed on the 5400 RPM HDD; blockfilterindex disabled (2026-09-10)

Superseded in part by D-002. Machine survey remains valid.

---

# Feasibility: can Omni run on Pepecoin?

**Verdict: yes.** Omni is a meta-layer, not a chain feature — it is a payload encoding plus
off-chain consensus rules, interpreted by an indexer. It needs no forked node, only a node
whose transaction policy will relay the payload-carrying outputs.

All of the following are ***verified in source code***, from
`github.com/pepecoinppc/pepecoin` at tag **v1.1.0** (commit `4fb5a0cd`, 2024-12-16),
cloned to `/home/martin/reference/pepecoin`.

| Requirement | Pepecoin v1.1.0 | Source | Verdict |
|---|---|---|---|
| **Class C** — `OP_RETURN` data output | `MAX_OP_RETURN_RELAY = 83` bytes (80 usable payload) | `src/script/standard.h:30` | ✅ identical to BTC/LTC |
| Data carrier relayed by default | `DEFAULT_ACCEPT_DATACARRIER = true` | `src/script/standard.h:16` | ✅ |
| Data size tunable | `-datacarrier`, `-datacarriersize` | `src/init.cpp:483-484,1094-1095` | ✅ |
| **Class B** — bare multisig relayed | `DEFAULT_PERMIT_BAREMULTISIG = true` | `src/validation.h:143` | ✅ |
| Bare multisig standardness | `x-of-3` accepted as standard | `src/policy/policy.cpp:41-48` | ✅ exactly what Class B needs (1-of-3) |
| Dust limit (soft) | `DEFAULT_DUST_LIMIT = RECOMMENDED_MIN_TX_FEE` = **0.01 PEP** | `src/policy/policy.h:70`, `:23` | ✅ cheap |
| Dust limit (hard, standardness) | `DEFAULT_HARD_DUST_LIMIT = DUST/10` = **0.001 PEP** | `src/policy/policy.h:81` | ✅ tunable via `-harddustlimit` |
| Recommended min fee | `RECOMMENDED_MIN_TX_FEE = COIN/100` = **0.01 PEP** | `src/policy/policy.h:23` | ✅ |
| Coin precision | `COIN = 100000000` (8 decimals) | `src/amount.h:18` | ✅ same as BTC/LTC |

**Other chain parameters** (*verified in source code*, `src/chainparams.cpp`):

| Parameter | Value | Line |
|---|---|---|
| Block spacing | **60 seconds** (Litecoin is 150 s, Bitcoin 600 s) | `:89` |
| Subsidy halving interval | 100,000 blocks | `:78` |
| AuxPoW (merged mining) from height | 42,000 | `:136` |
| AuxPoW chain ID | `0x003f` (63) | `:119` |
| Mainnet P2P port | 33874 | `:153` |
| Testnet P2P port | 44874 | `:282` |
| Regtest P2P port | 18444 | `:392` |

**Consequence of 1-minute blocks:** inscription chunks confirm ~2.5x faster than on Litecoin
and ~10x faster than Bitcoin. Good for multi-chunk file inscription, which is inherently
serial across many transactions.

---

# Corrections to two assumptions in the pivot

Martin cited "the check sum validator and built-in token exchange" as reasons to prefer Omni.
Both features are real, but one does not do what the name suggests.

### 1. Omni's "consensus hash" validates ledger state, not file content

`omni_getcurrentconsensushash` hashes the **entire Omni ledger** (balances, properties,
offers) at a given block, so two independent implementations can prove they agree.
It is an implementation-agreement check. It is **not** a per-inscription content checksum,
and it will not tell you whether a recovered JPEG is intact.

A content hash is still the right idea — it was already in the Phase 4 v2 spec
("a SHA-256 hash of the content"). That is ours to design; Omni does not supply it.
*Label: assumption pending Phase 1 verification against `consensushash.cpp`.*

### 2. The "built-in exchange" trades token ↔ base currency, probably not token ↔ token

Omni's **DEx** (types 20 offer / 22 accept) sells a token for the chain's native coin —
so on Pepecoin, token ↔ PEP. That works and transfers cleanly.

Omni Core on Bitcoin additionally has **MetaDEx** (types 25-28) for token ↔ token trading.
The transaction-type list Martin compiled from OmniLite does **not** include 25-28, which
suggests OmniLite lacks MetaDEx. If token-to-token trading matters, that capability may have
to be ported from Omni Core rather than OmniLite.
*Label: assumption — must be confirmed in Phase 1 against `src/omnicore/omnicore.h`.*

---

# The significant new risk: no reference implementation

On Litecoin, OmniLite exists. Our engine could be proven correct by replaying real mainnet
history and comparing `omni_getcurrentconsensushash` against a reference node at every block.

**On Pepecoin there is no Omni, so there is no oracle.** We would be the first and only
implementation, defining truth with no independent way to catch our own consensus bugs.
Also: on day one the DEx has no counterparties, and we must define our own genesis
parameters (activation height, Exodus-equivalent address, ecosystem IDs).

**Mitigation — and it removes the need for any 217 GiB sync:** OmniLite ships unit tests
(`src/omnicore/test/`) and functional tests (`test/functional/omni_*.py`) that run on
**regtest**. Those are executable, authoritative test vectors for the Omni rules, and they
need no mainnet chain at all. Building our engine against that suite gives real correctness
validation on this modest hardware. This was already the Phase 3 plan; it now carries more
weight because it is our *only* oracle.

# Recovery is not lost

Martin's 2021-2023 files are still in Litecoin blocks and are not going anywhere. Omni's
encoding (Class B / Class C, the payload layouts) is **chain-independent**, so if the
extractor is written chain-agnostically from the start — chain params, genesis height and
Exodus address as configuration, not constants — recovering them later costs a Litecoin
sync and a config file, not a rewrite.

**Design requirement adopted:** every component we build takes chain parameters as config.
No hard-coded Pepecoin assumptions.

---

## D-003 — App scope decisions (2026-09-10)

Decided by Martin:

| Question | Decision |
|---|---|
| First feature after the M0-M2 foundation | **M3 MetaDEx exchange** |
| Protocol marker (Class C, replaces Omni's `omni`) | **`pepo`** |
| Crowdsales, types 51/53 | **In scope** |

Marker note: `pepo` is consensus-critical. It can change freely before launch; after the
first mainnet inscription it cannot change without splitting state.

---

# ⚠️ MetaDEx constraints that affect the order-book design

*Verified in source code*, `omnicore/src/omnicore/tx.cpp:1643-1666` (`logicMath_MetaDExTrade`):

| Rule | Code | Consequence |
|---|---|---|
| Both sides are **property IDs** (`u32`) | payload layout, `createpayload.cpp:486` | **The native coin is not a property. MetaDEx cannot quote PEP.** |
| Non-fungible properties rejected | `isPropertyNonFungible(...)` → `PKT_ERROR_TOKENS -27`, `:1653` | **NFTs cannot trade on MetaDEx at all** |
| `property != desired_property` | `:1658` → `PKT_ERROR_METADEX -29` | no self-pairs |
| Both sides same ecosystem | `isTestEcosystemProperty(...)`, `:1666` | main and test books never mix |
| Type must be permitted for that property at that block | `IsTransactionTypeAllowed(...)`, `:1643` | activation rules are per-property |

## What this means in practice

**A TOKEN/PEP market with real bids and asks is not possible on MetaDEx.** Token ↔ native coin
is DEx only (types 20/22), which is **one-sided sell offers** — an offers list, not a book.

This is not an oversight in Omni; it is how Omni is structured. Omni's answer was a **base
property**: property 1 (`OMNI`) is a token, so every pair `TOKEN/OMNI` is a genuine two-sided
MetaDEx book, while `OMNI/BTC` is handled by the one-sided DEx.

For us the same shape applies: we need a **base property** — call it property 1 — against which
all MetaDEx pairs quote. Which raises the question Omni answered in 2013 with the Exodus
crowdsale: **how does property 1 come into existence and get distributed?**

Options:
1. **Crowdsale (type 51).** Send PEP to the issuer during a window, receive base tokens. This is
   literally what Omni did, and crowdsales are now in scope (D-003).
2. **Burn-to-mint.** Send PEP to the Exodus-equivalent address, receive base tokens at a fixed
   ratio. Simple, permissionless, ongoing, no issuer trust.
3. **Fixed issuance (type 50)** to a treasury address, distributed manually. Simplest to build,
   most centralised, weakest story.
4. **No base property.** Accept that only DEx (one-sided, token↔PEP) exists. Much weaker than
   what Martin asked for.

**Second open problem: NFTs cannot trade on MetaDEx** (`:1653`). Selling an NFT therefore needs
either the DEx path or a mechanism we design. Flagged for M4.

---

## D-004 — Protocol shape (2026-09-10)

Decided by Martin:

| Question | Decision | Consequence |
|---|---|---|
| Base property (property 1) origin | **Burn-to-mint** | Send PEP to the Exodus-equivalent address, receive base tokens at a fixed ratio, permanently open. No issuer trust, no deadline, no premine. Gives the base token a hard PEP cost floor. |
| NFT sales | **Extend DEx (20/22) to accept non-fungible ranges** | Deliberate, documented divergence from Omni Core, which rejects NFTs on MetaDEx (`tx.cpp:1653`) and has no NFT DEx path. Must be spec'd and tested as our own rule. |
| Activation height | **Set at launch, ~tip + 1 day** | Left unset in config until M7. Indexer backlog stays at zero through development. |
| Ecosystems | **Keep both (main + test)** | Mirrors Omni ID ranges; gives an on-mainnet sandbox that never mixes with the main book. |

### Consequences that now need specifying

1. **Burn-to-mint is a new transaction path with no Omni equivalent.** Omni's Exodus output is
   a *marker* for Class B, not a value sink. We are giving it a second meaning: PEP sent to it
   mints base tokens. These two roles must not collide — a Class B marker output must not
   accidentally mint. Needs an explicit, tested rule (e.g. minting only on an output that is
   the sole payment to Exodus in a transaction carrying no Omni payload, or a dedicated
   mint transaction type).
2. **NFT-on-DEx is our own consensus rule.** No reference implementation exists for it, so it
   carries the same "no oracle" risk as everything else, and needs its own test vectors.

---

## D-005 — Name, economics, signing, scope (2026-09-10)

Decided by Martin:

| Question | Decision |
|---|---|
| **Project name** | **Ribbit** |
| **Class C marker** | **`rbit`** (0x72 0x62 0x69 0x74) — **supersedes `pepo` from D-003** |
| Base token | **1 PEP = 1 RBIT**, divisible (8 dp), **uncapped**, minted by burn-to-mint only. No premine. |
| Signing | **Node wallet RPC** — app builds payload, `pepecoind` funds and signs via `fundrawtransaction` / `signrawtransaction`. Keys never enter our process. |
| Litecoin recovery (old Phase 2) | **Dropped.** |

### Effect of dropping Litecoin recovery

This is a real simplification, not just a deletion:

- **No v1 format reader.** The 4×255-char base64-in-type-50-text-fields format, and all its
  quirks (stray `'`, extra/missing `=`), leave the spec entirely. Inscriptions are **v2 only**.
- **No chain-agnostic requirement.** The design constraint adopted in D-002 is withdrawn.
  Chain parameters still live in config rather than as literals — that is ordinary good
  practice — but we no longer carry portability as a goal or test it.
- Martin's 2021-2023 files stay where they are, in Litecoin blocks. They are not going
  anywhere, but reversing this decision later would mean building the v1 reader and a
  Litecoin sync from scratch, not flipping a flag.

### Burn-to-mint, settled parameters

- Ratio **1:1**, divisible, uncapped, permanently open — no deadline, no cap, no issuer.
- Every RBIT therefore has a hard floor of 1 burned PEP behind it.
- Still to specify (see D-004): the rule that separates a **minting** payment to the Exodus
  address from a Class B **marker** output, so a Class B transaction can never mint by accident.

---

## D-006 — Openness, access, feature scope (2026-09-10)

| Question | Decision |
|---|---|
| Spec and code | **Public from the start** |
| Web UI binding | **127.0.0.1 only** — reach remotely via SSH port-forward |
| Base token ticker | **RBIT** |
| Delegates (73/74) | **In scope** |
| Freezing (71/72/185/186) | **In scope** |
| Send-to-owners (3) | **In scope** |
| Restricted send (2) | **Out of scope** |

**Why public matters more than it looks:** a second independent implementation is the only
genuine oracle for consensus correctness, and that can only exist if the spec is public. This
partially answers the "no reference implementation" risk from D-002 — not immediately, but it
is the only path that ever resolves it.

**Why localhost-only matters:** the app can trigger signing through the node wallet (D-005),
so exposing the UI exposes spending authority. Localhost is the correct default and costs
nothing, since SSH port-forwarding covers remote use.

---

## D-007 — No base token; burn-to-mint removed (2026-09-10)

**Decided by Martin:** no base token at all. **Supersedes D-004 and D-005 on this point.**

### What prompted it

Martin asked: *"If the Pepe is burned when the rbit is created, how would somebody convert
their RBIT back to Pepe?"*

**They cannot.** Ribbit is a sidecar meta-layer with **no consensus power over PEP** — only a
private-key holder can move PEP. Burning means sending to an address with no key, so the
protocol can never release it. This is structural. **No design gives trustless redemption of
the base coin**; any redemption requires a custodian holding real PEP. (Omni itself was
custodial here: its Exodus address held real BTC controlled by the foundation, not burned.)

### Correction to an earlier claim of mine

D-005 recorded that burn-to-mint gives RBIT *"a hard floor of 1 burned PEP."* **That was
wrong, and backwards.** Under uncapped 1:1 minting:

- 1 PEP is a permanent **ceiling** — nobody pays more than 1 PEP for something they can mint
  at 1 PEP.
- With no redemption there is **no floor whatsoever**.
- Secondary demand is ~zero, since anyone needing the token mints rather than buys, so holders
  can only exit by undercutting.

A quote asset is precisely what traders hold *between* trades, so this would have taxed every
user of the order book. Dropping it is the right call.

### What this removes

| Removed | Effect |
|---|---|
| RBIT the token, property 1 | no base/quote asset ships with the protocol |
| Burn-to-mint | **the Exodus address reverts to being purely a Class B marker, as in Omni** |
| Exodus dual-meaning rule | the "marker vs value sink" collision problem (D-004) disappears entirely |
| One of two unreferenced divergences | only NFT-on-DEx remains as novel consensus code |

Net: meaningfully less consensus surface, and less of it unvalidated. "Ribbit" and the `rbit`
marker are unaffected — they name the protocol, not a token.

### What the exchange looks like now

| Pair | Mechanism | Book shape |
|---|---|---|
| token ↔ token | **MetaDEx (25-28)** | **genuine two-sided book**, bids and asks |
| token ↔ PEP | DEx (20/22), payment window | one-sided sell offers |
| NFT ↔ PEP | DEx, extended for ranges (D-004) | one-sided listings |

No canonical quote asset ships. If the market converges on one — a wrapped-PEP or a stablecoin
issued by a participant — it becomes the de-facto quote asset without us blessing it. That is a
defensible position: we provide the venue, not the money.

### Implementation notes

1. **Property IDs 1 and 2 are special-cased in Omni** as `OMNI_PROPERTY_MSC` / `OMNI_PROPERTY_TMSC`
   (*verified in source*, `omnicore.h:123-124`). With no base token these are free. Decision:
   leave 1 and 2 **permanently reserved and unassigned**, user properties start at 3. Costs
   nothing and keeps a slot if a base token is ever revisited.
2. **Ported Omni test vectors assume OMNI/TOMNI exist.** Any vector that funds an account with
   property 1 or 2 needs adaptation. Expect friction in M2-M3 test porting; budget for it.

---

## D-008 — Crowdsales dropped (2026-09-10)

**Decided by Martin:** remove types 51 (create property variable / crowdsale) and 53 (close
crowdsale) from scope. **Supersedes D-003.**

**Rationale:** a crowdsale's payload carries `propertyIdDesired` (*verified in source*,
`createpayload.cpp:228-239`) — it can only be funded with **another property**, never the
native coin. With no base token (D-007), there is nothing to fund the first crowdsale with.
Keeping them would have meant either shipping a feature that cannot be used, or adding a
second novel consensus rule (native-coin crowdsale funding) immediately after deleting one.

**Removed:** bonus math, deadlines, early-close rules, issuer-percentage handling, and the
crowdsale-participation code path in state processing. A meaningful slice of M6.

**Consensus-hash note:** `GetConsensusHash()` has a crowdsale section
(*verified in source*, `consensushash.cpp:232-234`). We keep the section in our implementation
but it will always be empty, so it contributes nothing. Documented so that re-adding crowdsales
later does not change the hash of existing state.

**Reversible?** Yes, cleanly. Crowdsales touch property creation and balances but nothing else
depends on them.

---

## D-009 — Encrypted messaging: integration and carrier (2026-09-11)

Decided by Martin.

| Question | Decision |
|---|---|
| Where it lives | **Inside Ribbit** — a Ribbit message type sharing the payload codec, Class B/C encoding, indexer, reorg handling and database |
| Primary carrier | **Class B** (7,646 bytes/tx), **Class C** for messages fitting in 72 bytes |
| Messages > 7,646 bytes | **Chained Class B with a countdown index**, using the Doginals UTXO-chaining mechanism |

### Correction: the 25-transaction limit is not a ceiling

I had cited Pepecoin's 25-ancestor mempool limit as an argument against OP_RETURN
chunking. **Martin pointed out this is wrong**, and he is right: the limits are on
**in-mempool** ancestors (`init.cpp:437`, `:439`), and `removeForBlock`
(`txmempool.h:554`) drops mined transactions from the mempool, so a confirmation
resets the count. Send 25, wait one block (~60 s), send 25 more. It is a
throughput constraint, not a ceiling.

The real arguments for Class B are **atomicity** and **cost**:

- A single-transaction message either confirms whole or not at all. A
  16-transaction message has 16 chances to half-land, and a partial message is
  on-chain **permanently** — unreadable but not removable.
- It removes an entire category of design: no chunk index, count, gap detection,
  reassembly or chaining for anything under ~7,598 characters.
- ~0.022 PEP in one transaction versus ~0.04 PEP across sixteen.

### A size limit that binds before the count does

*Verified in source*, `validation.h:76,80`:
`DEFAULT_ANCESTOR_SIZE_LIMIT = DEFAULT_DESCENDANT_SIZE_LIMIT = 101` kB.

An unconfirmed chain is capped at ~101 kB **regardless of transaction count**.
Irrelevant on the OP_RETURN path (25 × ~250 B ≈ 6 kB, so the count binds first),
but decisive for chained Class B: at ~14.7 kB per full transaction only **~6** can
chain unconfirmed, not 25. Martin's batch-and-wait strategy applies exactly here.

---

# What Doginals does, and what we take from it

Read from Martin's own `github.com/martinseeger2002/doginals`, `doginals.js:434-560`.

```js
MAX_CHUNK_LEN   = 240    // bytes per data push
MAX_PAYLOAD_LEN = 1500   // bytes per transaction
```

1. Data is split into 240-byte chunks. The inscription script is `'ord'`, chunk
   count, content type, then `(index, chunk)` pairs where the index **counts
   down**: `parts.length - n - 1`, so **the final chunk carries index 0**.
2. Chunks are packed into ~1500-byte partials, one per transaction.
3. Each transaction creates a **P2SH output** committing to its partial. The
   redeem script is only `<pubkey> OP_CHECKSIGVERIFY <OP_DROP × n> OP_TRUE`.
4. The **next** transaction spends it, and the data rides in that transaction's
   **scriptSig** as plain pushes, which the `OP_DROP`s discard.

### Why 1500 is not arbitrary

*Verified in source*, `pepecoin/src/policy/policy.cpp:78-93`:

```
scriptSig.size() > 1650   ->  "scriptsig-size", non-standard
!scriptSig.IsPushOnly()   ->  "scriptsig-not-pushonly"
```

plus `MAX_SCRIPT_ELEMENT_SIZE = 520` (`script.h:22`). So ~1500 bytes of data, a
~72-byte signature and a ~40-byte redeem script land just under 1650. The
constant is tuned precisely to that ceiling. **The technique is mainnet-standard
on Pepecoin.**

### The two properties worth stealing

- **Ordering is enforced by the UTXO chain, not by trusting an index.** A chunk
  cannot be reordered or skipped, because each transaction spends the previous
  one's output. Only whoever holds that output can extend the chain — which is
  the sender binding the brief wanted from change-chaining, except structural
  rather than conventional.
- **Completion is self-describing.** The countdown reaching 0 *is* the
  terminator. An abandoned chain never reaches 0, so an incomplete message is
  always distinguishable from a complete one — no separate seal transaction, no
  ambiguity.

### Carrier comparison

| Carrier | Bytes/tx | Atomic | Ordering | Mainnet-standard |
|---|---|---|---|---|
| Class C (OP_RETURN) | 72 | ✅ | needs index | ✅ |
| **Class B (multisig)** | **7,646** | ✅ ≤7,646 | n/a, single tx | ✅ |
| Doginals P2SH | ~1,500 | ❌ | **UTXO chain** | ✅ |

**Decision: borrow the mechanism, not the carrier.** Chained *Class B* with a
countdown index gives Doginals' structural ordering and self-describing
completion while carrying **5× more data per transaction**.

### Resulting shape

| Message size | Carrier | Chunking machinery |
|---|---|---|
| ≤ 72 bytes | Class C, one transaction | none |
| ≤ 7,646 bytes | **Class B, one transaction** | **none** |
| larger | chained Class B, countdown index | chunk header only on this path |

---

## D-010 — The Messenger is testnet-only, permanently (2026-09-11)

**Decided by Martin:** *"The Messenger part of the application should always use testnet."*

This is a **permanent product rule**, not a testing phase. Encrypted messaging
never operates on mainnet.

### What this changes

| Area | Effect |
|---|---|
| Network selection | The messaging code path **refuses to run against mainnet**, enforced in code rather than by configuration. A mainnet params object passed to a messaging function is an error, not an option. |
| Mainnet standardness | Still designed for. Testnet does not enforce standardness (`fRequireStandard = false`, `chainparams.cpp:309`), so a transaction that only works there would be silently non-standard. Keeping to mainnet rules means the format stays honest and portable. |
| Threat model | Testnet chains **can be reset or deep-reorged**. Messages are not durable. This must be stated plainly to users — it is a property of the medium, not a bug. |
| Coins | Testnet PEP has no value, so fee estimates are informational only. |
| Node | Requires a **second node** alongside the mainnet Ribbit node. |

### Why designing to mainnet limits still matters

Testnet accepts non-standard transactions. Without the discipline of mainnet
limits we would not discover that a format is unrelayable until someone tried it
somewhere real. The regtest standardness check (`-acceptnonstdtxn=0`) in Step 4
is what actually proves it.

---

## D-011 — Renamed to DogecoinArcade; multi-chain (2026-09-11)

**Decided by Martin:** *"Let's change the name of the application to DogecoinArcade.
I know it's for Pepe, but it should also work on Doge in the exact same way."*

| Item | Old | New |
|---|---|---|
| Project | Ribbit | **DogecoinArcade** |
| Class C marker | `rbit` | **`arcd`** (0x61 0x72 0x63 0x64) |
| Python package | `ribbit` | **`arcade`** |
| Distribution name | `ribbit` | **`dogecoin-arcade`** |
| Repo path | `~/ribbit` | `~/dogecoin-arcade` |

**Entries D-001 through D-010 above still say "Ribbit".** They are left as written:
a decision log that gets retroactively edited is not a record. Read "Ribbit" as
"DogecoinArcade" in anything dated before 2026-09-11.

The marker change was free because nothing has launched. **After the first
mainnet transaction it would be impossible** — a marker change splits state.

### Relationship to the existing GitHub repo

`github.com/martinseeger2002/DogecoinArcade` already exists and already contains
encrypted messaging (`SendSms.py`, `DecryptSmsData.py`, `encrypt_data.py`,
`callGetPubKey.py`). **Martin's decision: keep them entirely distinct.** This is a
new project that shares a name; nothing is merged and nothing is pushed.

---

# Dogecoin support: verified, and nearly free

Verified against `github.com/dogecoin/dogecoin` at 1.14.99, cloned to
`/home/martin/reference/dogecoin`.

Every constant this protocol depends on is **identical** between Dogecoin and
Pepecoin — same values, and the same source line numbers, because Pepecoin is a
Dogecoin fork:

| Constant | Value | Source (identical in both) |
|---|---|---|
| `MAX_OP_RETURN_RELAY` | 83 | `script/standard.h:30` |
| `DEFAULT_ACCEPT_DATACARRIER` | true | `script/standard.h:16` |
| `DEFAULT_PERMIT_BAREMULTISIG` | true | `validation.h:143` |
| x-of-3 bare multisig standard | yes | `policy/policy.cpp:41` |
| `COIN` | 100000000 | `amount.h:18` |
| `RECOMMENDED_MIN_TX_FEE` | COIN/100 | `policy/policy.h:23` |
| `DEFAULT_DUST_LIMIT` | = min fee | `policy/policy.h:70` |
| `DEFAULT_HARD_DUST_LIMIT` | DUST/10 | `policy/policy.h:81` |
| scriptSig standardness limit | 1650 | `policy/policy.cpp:86` |
| `DEFAULT_ANCESTOR_LIMIT` / size | 25 / 101 kB | `validation.h:74,76` |
| Block spacing | 60 s | `chainparams.cpp` |

**Only chain identity differs:**

| | Dogecoin | Pepecoin |
|---|---|---|
| mainnet P2PKH version | 30 (`D…`) | 56 (`P…`) |
| mainnet P2SH version | 22 | 22 |
| mainnet P2P / RPC | 22556 / 22555 | 33874 / 33873 |
| testnet P2P / RPC | 44556 / 44555 | 44874 / 44873 |

Because `Params` already holds exactly these fields, Dogecoin support cost
**three new `Params` instances and zero code changes**.

### ⚠️ Testnet addresses are ambiguous between the two chains

Dogecoin testnet and Pepecoin testnet both use `PUBKEY_ADDRESS = 113` and
`SCRIPT_ADDRESS = 196`. **A testnet address cannot tell you which chain it is
for.** Never infer the chain from an address; record it explicitly alongside any
key announcement or message. Noted in `arcade/config.py` beside the params.

### Messaging guard

`require_messaging_network()` enforces D-010 in code: messaging functions refuse
any mainnet params object. `MESSAGING_NETWORKS` is `{test, regtest, doge-test,
doge-regtest}`.

---

# Martin's existing encryption, compared to the new brief

Read from `github.com/martinseeger2002/DogecoinArcade`, `encrypt_data.py`.

**Existing scheme:** secp256k1 ECDH → HKDF-SHA256 → AES-256-GCM key-wrap →
AES-256-GCM data.

| | Existing | Brief requires |
|---|---|---|
| Curve | secp256k1, using **wallet keys** | X25519, **separate from wallet** |
| Construction | ECDH + HKDF + AES-GCM keywrap + AES-GCM | libsodium sealed box |
| Envelope overhead | **93 bytes** (33 pubkey + 12 IV + 32 wrapped key + 16 tag) | **48 bytes** (32 ephemeral pubkey + 16 MAC) |

The existing construction is cryptographically sound — ECDH+HKDF+AES-GCM is a
legitimate KEM/DEM. Two reasons the new design still differs:

1. **45 bytes of pure overhead.** A sealed box derives the data key directly from
   the ECDH output instead of wrapping a separate AES key. On an 80-byte
   OP_RETURN that difference decides whether a short message fits at all.
2. **Wallet keys are the wrong keys.** Encrypting to a spending key welds
   messaging identity to spending authority: the key is exposed by every
   signature, cannot be rotated without moving funds, and its compromise reads
   every past message. Separate X25519 identity keys fix all three.

---

## D-012 — The application is permanently dual-chain (2026-09-11)

**Decided by Martin:** *"the page should have both testnet and mainnet on it
forever because we will be using testnet for messages and main for tokens and
nfts and also pep transactions."*

| Chain | Role | Sections |
|---|---|---|
| **Testnet** | Messenger | Inbox, Compose, Keys |
| **Mainnet** | Ledger | Tokens, NFTs, Exchange, PEP transactions, Explorer |

This is a permanent split, not a migration path. It supersedes the implicit
assumption in the web UI that one network was selected at launch.

### Why the split makes sense

Messages are **not durable**: testnet chains get reset and deep-reorged (D-010),
and messages are conversational rather than assets. Tokens, NFTs and PEP balances
**are** assets and belong on a chain nobody resets. Each side gets the chain whose
properties match what it holds.

It also means a bug in the messaging code can never touch real value, which is a
stronger guarantee than any amount of care in the messaging code itself.

### The immediate constraint

The mainnet node runs with **`disablewallet=1`** (docs/00-node-setup.md), chosen
in Phase 0 because nothing then needed to sign. So mainnet is currently
**read-only**: it can serve blocks, indexed token state and NFT state, but cannot
send PEP or create a token.

Enabling a mainnet wallet is a deliberate step Martin has to take, because that
wallet holds real money. Until then the UI must show mainnet sections as
read-only rather than failing obscurely at signing time.

### Guards that must hold

1. `require_messaging_network()` continues to refuse mainnet for anything in
   `arcade.messaging`. The dual-chain UI makes it *easier* to point a messaging
   call at the wrong node, not harder, so this matters more now, not less.
2. `verify_connected_chain()` runs on every connection, so a misconfigured
   datadir cannot silently cross the streams.
3. Every section states which chain it is acting on, in the interface itself. A
   user who cannot tell whether they are about to spend testnet or real coins is
   one misclick from a bad day.

---

## D-013 — The wallet is the identity; there is no passphrase (2026-09-11)

**Decision.** The messaging identity is derived from a wallet address. There is
no passphrase, no key file, and nothing for a user to write down. `wallet.dat`
is the only thing they have to keep.

### What this replaces

D-009 gave the messaging key its own passphrase, encrypted at rest with Argon2id.
That was cryptographically sound and a poor product. The failure mode was
absolute: forget the passphrase and every message ever sent to that identity is
unreadable, by anyone, forever. Two attempts were made to soften it — generating
the passphrase rather than letting the user invent one, then saving it on the
machine so it could be looked up later — and the second undoes most of the point
of the first. Storing a secret next to the thing it protects is not protection;
it is a passphrase-shaped ritual.

Martin's instruction was direct: *"I don't want a pass phrase to be saved or
shown. The only thing that the user should need is their wallet.dat files."*

### How it works

    signature = wallet.signmessage(address, "DogecoinArcade messaging identity v1")
    seed      = HKDF-SHA256(signature)
    identity  = X25519 keypair from that seed

`signmessage` is deterministic — RFC 6979 — so the same address always produces
the same signature and therefore the same identity. Verified on Pepecoin:
three calls, byte-identical.

The address is filed in the wallet under the account `arcade-identity`. Accounts
live inside wallet.dat, which is what makes a restore self-sufficient: the
application finds the address again without needing its own database.

### The subtle part, which was a bug first

`getaccountaddress` returns a **fresh** address as soon as the current one has
been used. Calling it on each startup would therefore have changed the identity
silently, orphaning every message already received — the exact failure the
passphrase was removed to avoid. Caught in testing, before it shipped.

The rule is now: take the account's existing addresses, sort them, use the
first. `getaccountaddress` is reached only when the account is genuinely empty.
Three independent installations against one wallet derive the same identity, and
the CLI and the web interface agree with each other.

### What it costs, stated plainly

**Whoever can use the wallet can re-derive the identity.** The wallet's security
becomes the messaging security; if the wallet is unencrypted, so is the identity
in practice. Against a standalone key with its own passphrase this is a real
reduction, and it is the trade being made deliberately: the passphrase's failure
mode was certain and common, this one requires an attacker to already have the
wallet — at which point they have the coins too.

A spending key is still never used for encryption. Deriving a separate X25519
key keeps encryption away from spending, so a compromise of one is not
automatically a compromise of the other, and the messaging key can be rotated by
moving to a different address without moving funds.

### Consequences

- Fingerprints are gone from the interface. They named the identity in a form no
  user could act on. Names from the address book come first, then the address.
  The CLI keeps them: it is a diagnostic tool and that is where they belong.
- `vault.py` — the credential store that remembered passphrases — is deleted.
- Key files still work when named explicitly with `--key`, for identities made
  before this. They are never picked up implicitly, because the web interface
  derives from the wallet and the two halves of the application answering to
  different identities would be worse than either choice alone.
- Backup and restore had to become real features rather than documentation:
  `arcade/backup.py` and the Backup page. If the wallet is the only thing that
  matters, keeping it has to be something a person can actually do.

---

## D-014 — Public group posts may run on mainnet (2026-09-11)

**Decision.** A public, unencrypted group channel runs on **both** chains. D-010
is unchanged: nothing encrypted ever touches mainnet.

### Why this is not a reversal of D-010

D-010 says *the Messenger* is testnet-only, permanently, and the reasons were
specific: messaging code is young, a bug in it must never be able to touch real
value, and an encrypted message carries key material that would tie a mainnet
identity to a correspondence. None of that applies to a public post.

A group post is not sealed to anybody. It carries no key material. It reveals
nothing that publishing it does not already reveal, because publishing it *is*
the act. So the question "may this run on mainnet?" stops being a safety question
and becomes a cost question, which is the user's to answer. Martin answered it.

### How the boundary is kept

`MessageSender` still calls `require_messaging_network()` on construction. The
only way past it is `public_only=True`, which is:

- opt-in, never a default;
- named for what it permits rather than what it disables, so a call site asking
  for mainnet has to say out loud that what it is sending is public;
- passed by exactly one call path, the group-post route;
- covered by a test asserting that encrypted sending is still refused on `main`
  and `doge-main`.

### Carriage, and why a post is short

Class C: one OP_RETURN output. One output means **no dust**, so a post costs a
fee and nothing else. That caps a post at about 60 characters.

That is a real limit and the right trade. Class B would carry 7.5 KB, but it pays
roughly one unspendable output per 30 bytes — on testnet that is free coins, and
on mainnet it is somebody's money burnt to say something in public. A 60-character
public post costs about 0.0028 in fees; the same text carried Class B would cost
that plus several coins of dust forever.

### What the interface must say

The rest of the application trains the expectation that messages are private, so
the public half has to contradict that expectation loudly rather than quietly:

1. Every screen where a post can be written states that nothing there is
   encrypted, that it is permanent, and that it cannot be deleted.
2. The two chains are chosen explicitly, labelled *free coins, nothing at stake*
   and *real coins, permanent*. There is no single control that silently spends
   money.
3. A mainnet post shows the post's text back to the user inside the confirmation,
   with the fee, before anything is broadcast — the ground rule for mainnet
   transactions, applied here.

### Channels have no membership

A channel is a name. Anyone using the same name is in the same room; there is no
invitation, no member list, no way to remove anyone, and no moderation. Saying so
plainly is the whole of the access-control design, and pretending otherwise would
be worse than the limitation.
