psffpp-payments
CashScript payout contracts for PSFFPP (Permissionless Software Foundation File Pinning Protocol) deposits on Bitcoin Cash.
Users send BCH to a single PoolContract address. Once per payout epoch (blockheight-gated), the pool splits 20% to treasury and 80% into three ShareContract UTXOs (one per node). A K-of-M (2-of-3) operator quorum must sign before any share pays out.
Project docs live in docs/. Start with Why CashScript for the high-level motivation (replacing PSF-token proof-of-burn with pure BCH payouts). External references: CashScript · Design: circular-economy trust-minimized federation (M=3, K=2).
Scope (v1)
| In scope | Out of scope |
|---|---|
ShareContract.cash, PoolContract.cash |
Cron (consolidate/split/broadcast) |
| Compile + mock tests | Timecard web GUI / sig collection |
| Address + deploy record scripts | Treasury multisig, withdrawal caps |
Epoch model
Each Pool deploy bakes a single splitBlockheight. After that height, split() may run once the pool is consolidated. Later months are not gated by the same contract: deploy a new Pool+Shares with the next height, then K-of-M migrate() any remaining pool value into the new Pool.
Contracts
ShareContract
Constructor: node0, node1, node2 pubkeys + this node's nodePkh.
claim(s0, s1, s2)— requires ≥2 valid ECDSA sigs against registered pubs; pays exactly tonodePkh; terminating (no self-replication). Unused sigs must be empty (0x).
PoolContract
Constructor: three pubs, treasuryPkh, three share locking bytecodes, minConsolidation (100_000), splitBlockheight.
consolidate()— permissionless; outputs must self-replicate (same P2SH locking bytecode), pure BCH, ≥minConsolidation.split()— permissionless aftertx.time >= splitBlockheight; 4 outputs: treasury (20% + rounding remainder) + three equal shares locked in the baked ShareContracts. Prefer a companion fee-paying input so the full pool value is apportioned.migrate(s0, s1, s2)— 2-of-3; destinations are authorized by the signatures (no baked next-pool bytecode).
Setup
npm install
npm run compile
npm test
Requires Node.js ≥ 22. Matches cashc / cashscript ^0.13.
Config
cp config/deploy.mainnet.example.json config/deploy.mainnet.json
Fill in:
- Three compressed operator pubkeys (66 hex chars)
- Three node payout pkh values (40 hex chars) — usually
hash160(pubkey)for each operator’s payout key - Treasury pkh
splitBlockheight(absolute BCH block height for this epoch)minConsolidation(default 100000)
config/deploy.mainnet.json is gitignored. Never put private keys in config.
Deploy order (mainnet)
Contracts are immutable. Order matters:
- Freeze operator pubs / PKHs / treasury PKH /
splitBlockheight. npm run compilenpm run addresses -- config/deploy.mainnet.json— prints three Share addresses + Pool address.npm run deploy -- config/deploy.mainnet.json— writesdeployments/<timestamp>.json(addresses, locking bytecodes, artifact fingerprints). Does not move funds.- Publish Pool P2SH32 address as the deposit address.
- Smoke checklist (small intentional funding only):
- Deposit a small amount to the Pool
- Run
consolidateif multiple UTXOs - Confirm
splitfails before the gate - After height:
splitwith a fee-paying companion input claima share with 2-of-3 ECDSA signatures- Optionally test
migrateto a next-epoch Pool
- Only then accept production deposits
Signing (ECDSA)
import { SignatureTemplate, HashType, SignatureAlgorithm } from 'cashscript'
const tmpl = new SignatureTemplate(
privateKey,
HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS,
SignatureAlgorithm.ECDSA
)
Pass Uint8Array.of() for unused signature slots.
Next epoch
- Deploy new Shares + Pool with the next
splitBlockheight. - K-of-M
migrateremaining old-pool value to the new Pool address. - Claim any unclaimed old Share UTXOs separately (still valid under the old Share contracts).
Security notes
- Limit outputs first in every function.
- Consolidate compares against
tx.inputs[this.activeInputIndex].lockingBytecode(P2SH wrapper), not redeem script. - Empty
tokenCategoryon covenant outputs (pure BCH). - Use
tx.time >=(never>). - Fee stalls on claim/migrate require rebuilding the tx and re-collecting all K signatures.
- If ≥K operators collude they can approve any signed migrate destination — trust-minimized federation, not permissionless.
License
MIT