← docs

Step 1 — Pepecoin node for encrypted messaging

Status: 1.1 complete. 1.2–1.4 await Martin's approval. Date: 2026-09-11

Docs for this workstream live in docs/messaging/ to avoid colliding with the Ribbit numbering already in docs/ (00-node-setup, 03-companion-app-design, M0–M2).


1.1 — Binary

Machine

The brief again describes an NVIDIA DGX Spark (GB10), aarch64. This is not that machine — same discrepancy as at the start of the Ribbit work, which Martin resolved then with "this pc is not the spark it's the mini pc".

Verified on host: uname -mx86_64, product name JB20B. Intel Celeron N5095A, 4 cores, 15 GiB RAM, one 5400 RPM HDD, 415 GiB free.

Consequence: we need the x86_64 build, not aarch64. The aarch64 tarball would not execute here.

Release check

Verified via GitHub API, 2026-09-11 — the three most recent releases:

Tag Published
v1.1.0 2024-12-18
v1.0.1 2024-05-14
v1.0.0 2024-01-30

v1.1.0 is still the latest. Nothing newer, confirming the brief.

Already installed and verified

Pepecoin Core v1.1.0 was installed during the Ribbit work and does not need redoing. From docs/00-node-setup.md:

Check Result
SHA-256 9d7ef948…f710b, matches SHA256SUMS.asc
GPG signature Good — key 18250EC9 2E527E97 23E49116 FD415169 D2691927
Signer David Eichel, fingerprint matches contrib/gitian-keys/david2278-key.pgp in the repo
Binary ↔ source pepecoind reports v1.1.0.0-4fb5a0cd9 = the v1.1.0 tag commit
Installed at /usr/local/bin/{pepecoind,pepecoin-cli,pepecoin-tx}

Two caveats stand from that verification: the signing key expired 2026-01-02 (the signature predates expiry, so it is sound, but nothing has been re-signed), and trust is single-channel — tarball, checksums, signature and key all come from the same GitHub org, and the key is not on public keyservers.

No new download was needed, so nothing new to verify.


Re-verification of the facts in the brief

All verified in source code at /home/martin/reference/pepecoin, tag v1.1.0 (commit 4fb5a0cd), except where noted.

Claim Verdict Evidence
pepecoinppc/pepecoin, Dogecoin-derived binary banner: "Bitcoin Core, Dogecoin Core, and Pepecoin Core"
Latest release v1.1.0 GitHub API
Testnet P2P 44874 chainparams.cpp:282
Testnet RPC 44873 chainparamsbase.cpp:48
Testnet addresses start with n PUBKEY_ADDRESS = 113; encoding a test hash160 gives nhixVBZHWLHo9YEm6AbZre38n7EaMi9UNw
OP_RETURN script ≤ 83 bytes script/standard.h:30MAX_OP_RETURN_RELAY = 83
~80 bytes of data 83 − 1 (OP_RETURN) − 2 (OP_PUSHDATA1 + len) = 80
Only one OP_RETURN per standard tx policy/policy.cpp:115-119if (nDataOut > 1) reason = "multi-op-return"
Mainnet fRequireStandard = true chainparams.cpp:177
Testnet / regtest fRequireStandard = false chainparams.cpp:309, :408
-acceptnonstdtxn toggles it init.cpp:1057, and it is refused on mainnet (init.cpp:1059)
signrawtransaction, not …withwallet present / absent in src/rpc, src/wallet/rpcwallet.cpp
validateaddress, not getaddressinfo same
generate and generatetoaddress both exist same
fundrawtransaction exists same
Recommended min fee 0.01 PEP/kB policy/policy.h:23RECOMMENDED_MIN_TX_FEE = COIN/100
Dust limit 0.01 PEP policy/policy.h:70 — and note the hard dust limit is DUST/10 = 0.001 PEP (:81), which is what standardness actually enforces
Mempool ancestor/descendant limits 25 validation.h:74, :78
aarch64 gitian target exists release assets include pepecoin-1.1.0-aarch64-linux-gnu.tar.gz — irrelevant here, this host is x86_64

Every claim in the brief holds. One refinement worth carrying into the design: there are two dust limits, and the one that decides standardness is the hard limit of 0.001 PEP, not the 0.01 PEP soft limit.


⚠️ Finding that changes Step 2 materially

The brief assumes messages are chunked across 80-byte OP_RETURN outputs, with transactions chained through change outputs and a 25-transaction ceiling.

Ribbit already has a much larger carrier. From M1, verified by test:

Carrier Payload bytes per transaction
Class C (OP_RETURN) 72 after the 4-byte marker and 4-byte type header
Class B (bare multisig) 7,646

Class B is standard on Pepecoin mainnet — verified in source code: DEFAULT_PERMIT_BAREMULTISIG = true (validation.h:143) and x-of-3 bare multisig is standard (policy/policy.cpp:41-48).

What that does to the chunking problem

Sealed-box overhead is 48 bytes (32-byte ephemeral public key + 16-byte MAC). Assuming a 12-byte chunk header, OP_RETURN carries ~68 ciphertext bytes per transaction:

Message Ciphertext OP_RETURN path Class B path
100 chars 148 B 3 transactions 1
500 chars 548 B 9 transactions 1
1,000 chars 1,048 B 16 transactions 1
7,598 chars 7,646 B 113 tx — over the 25 limit 1

(assumption — exact figures recomputed in Step 2 once the header is fixed)

Class B also appears cheaper, not merely fewer transactions: 1,048 bytes needs 35 packets → 18 multisig outputs → roughly a 2.2 KB transaction ≈ 0.022 PEP at 0.01 PEP/kB, against 16 OP_RETURN transactions of ~250 B each ≈ 0.04 PEP.

Other consequences

Recommendation: Step 2 should design for Class B as the primary carrier and keep Class C for short messages that fit in 72 bytes, rather than treating 80-byte OP_RETURN chunking as the only option. Both remain mainnet-standard.


Open question before 1.2

Does this messaging feature live inside Ribbit (its own message type, sharing the codec, indexer and database) or as a separate tool? Martin's instruction was "Add encrypted messaging to Ribbit", which suggests the former, but the brief is written standalone. This decides whether messages get a Ribbit message type or their own magic bytes.


1.2 — Testnet service: done

Installed alongside the mainnet node. Neither touches the other's data.

Mainnet Testnet
Unit pepecoind.service pepecoind-testnet.service
Datadir /var/lib/pepecoind /home/martin/.pepecoin-testnet
Runs as pepecoin martin
P2P / RPC 33874 / 33873 44874 / 44873
ZMQ 28332/3/5 28432/3/5
Auth rpcauth cookie
Wallet disabled enabled
dbcache 4096 MiB 512 MiB

Verified on host: both services active, all six ZMQ ports listening, no collisions.

Why testnet runs as martin and mainnet does not. The brief requires cookie auth. pepecoind always writes .cookie at mode 0600, and no group membership, -rpccookiefile path or POSIX ACL can share a 0600 file across users — we proved this the hard way on mainnet, which is why mainnet ended up on rpcauth instead. Running the testnet daemon as martin makes cookie auth work with no credential sharing at all. Testnet coins have no value, so nothing is at risk.

The AF_NETLINK fix was carried into this unit from the start, so it did not repeat the mainnet ZMQ crash loop.

Mainnet, incidentally: fully synced

Height 1,204,728, tip block 48 seconds old, 8 peers, 12.7 GB on disk. The whole chain synced overnight on the 5400 rpm disk. Pepecoin is a young, low-volume chain — far smaller than the Litecoin figures that framed the original storage planning.


1.3 — Peers and sync: confirmed

Both testnet DNS seeds are compiled in (chainparams.cpp:95-96) and resolve; 2 of 4 seed IPs accepted connections on 44874. The node has 2 peers and is syncing at ~24 blocks/sec.

The testnet chain is deeper than it first appeared — headers were still climbing past 450,000 at the time of writing. At the observed rate this is a few hours. A background watcher is running.


1.4 — Testnet coins: CPU mining is realistic; no faucet needed

The decisive rule

Verified in source code, pow.cpp:17-28 and chainparams.cpp (testnet):

bool AllowMinDifficultyForBlock(...)
{
    if (!params.fPowAllowMinDifficultyBlocks) return false;
    if (pindexLast->nHeight < 1250)            return false;
    // Allow a minimum-difficulty block if more than 2*nTargetSpacing has elapsed
    return (pblock->GetBlockTime() > pindexLast->GetBlockTime() + params.nPowTargetSpacing*2);
}

The testnet ruleset progression is:

Heights Ruleset Min-difficulty allowed
0–999 base true
1000–1249 digishield false
1250–41,999 minDifficulty true
42,000– auxpow (= minDifficultyConsensus) true (inherited)

So at present heights the rule is active: if testnet has been quiet for more than 120 seconds (2 × 60 s spacing), the next block may be mined at powLimit = 0x00000fff… — roughly 2²⁰ expected Scrypt hashes.

A 4-core Celeron N5095A should find such a block in on the order of a minute (assumption — order-of-magnitude only, to be measured with generate once the sync completes). Coinbase maturity on testnet is 240 blocks (digishieldConsensus.nCoinbaseMaturity = 240), so mined coins are spendable after ~240 minutes of chain time, or immediately-ish if we mine a run of blocks.

Conclusion: a faucet is a convenience, not a dependency. We can mint our own testnet coins with the built-in generate RPC.

Faucets, for completeness

pepeblocks.com/faucet exists and testnet.pepeblocks.com resolves and returns HTTP 200, but both are client-rendered SvelteKit apps, so it could not be confirmed from the HTML whether the faucet dispenses testnet coins or only mainnet. Worth a manual try once we have a testnet address; not worth blocking on.


Future requirement: cross-platform installer

Martin, 2026-09-11: "Eventually I want an installer that will automatically set up core and testnet. I will want one for Windows and Linux and an ARM variant."

Recorded as a requirement, not built yet. Notes for when it is:

Upstream ships the binaries we would need

Verified via GitHub API, v1.1.0 release assets:

Target Asset Relevant
Linux x86_64 pepecoin-1.1.0-x86_64-linux-gnu.tar.gz
Linux aarch64 pepecoin-1.1.0-aarch64-linux-gnu.tar.gz ✅ ARM variant
Linux armhf (32-bit) pepecoin-1.1.0-arm-linux-gnueabihf.tar.gz ✅ Raspberry Pi 32-bit
Windows 64 pepecoin-1.1.0-win64.zip, …-win64-setup-unsigned.exe
macOS pepecoin-1.1.0-osx-unsigned.dmg (not requested)

So the installer never needs to build from source on any requested platform.

What differs per platform, and what does not

Same everywhere: download URL pattern, SHA-256 verification, GPG verification against contrib/gitian-keys/david2278-key.pgp, and the testnet config file contents.

Differs:

Linux Windows
Service manager systemd unit Windows Service or Scheduled Task (no systemd)
Default datadir ~/.pepecoin %APPDATA%\Pepecoin
Binary location /usr/local/bin %ProgramFiles%\Pepecoin
Privilege model sudo UAC elevation
Path separator, line endings both differ

Lessons from doing it by hand that the installer must encode

Every one of these cost us a debugging cycle in this session:

  1. RestrictAddressFamilies must include AF_NETLINK, or libzmq aborts at startup with ip_resolver.cpp:542.
  2. -daemonwait does not exist in this 0.13/0.14-era codebase. Use Type=simple and run in the foreground.
  3. The rpcauth HMAC uses the salt as raw ASCII of the hex string, not decoded hex (httprpc.cpp:118). Getting this wrong yields a silent auth failure.
  4. .cookie is always mode 0600, so cookie auth only works if the client runs as the same user as the daemon. Plan the service user around that, not after it.
  5. The signing key is expired, and is not on public keyservers — the installer must ship or fetch it from the repo and should warn rather than fail.
  6. Verify before executing, always: hash and signature, and stop on failure.

Open questions for that work