Trades
This commit is contained in:
+17
-13
@@ -17,7 +17,7 @@ implementation-specific, and changing them can create a different chain.
|
||||
|
||||
## Network, genesis and article selection
|
||||
|
||||
- Network: `wikideck-randomx-testnet-5`, block version **5**. Genesis has no blocks, NFTs or work; first mined height is 1. No privileged issuance/admin mint key. Version 5 has a new chain identity; prior-version ledger files and wallet backups are not compatible.
|
||||
- Network: `wikideck-randomx-testnet-6`, block version **6**. Genesis has no blocks, NFTs or work; first mined height is 1. No privileged issuance/admin mint key. Version 6 adds atomic NFT trade bundles and has a new chain identity; prior-version ledger files and wallet backups are not compatible.
|
||||
- `GENESIS_TIME = 1792411200` (Unix seconds). Let `S(x)` mean lowercase hex SHA-256 of UTF-8 `x`, and `J(x)` mean compact JavaScript `JSON.stringify(x)`; field order below is consensus-critical.
|
||||
- `MAX_TARGET` is `0fff` followed by 60 `f` digits. `CHAIN_ID = S(J([NETWORK, CONSENSUS, GENESIS_TIME, MAX_TARGET]))`, using the constants object's insertion order in the implementation. `CONSENSUS` identifies the random Wikipedia source and mint-statistics snapshot format.
|
||||
- `GENESIS_HASH = S(NETWORK + ':genesis:' + CHAIN_ID)`; `INITIAL_KEY = S(NETWORK + ':initial-key:' + CHAIN_ID)`. Nodes independently derive the same manifest and reject other network/chain IDs.
|
||||
@@ -49,40 +49,44 @@ The miner signs `J([NETWORK, 'block-claim', blockID])` using ECDSA P-256/SHA-256
|
||||
|
||||
## Native NFT transition and commitments
|
||||
|
||||
Each block executes its ordered transfers against its parent's NFT state, **then mints one NFT** to the block owner. State entries contain owner, sequence (initially 0), minted height and page ID. Transfers cannot spend this block's new issuance.
|
||||
Each block executes its ordered transactions against its parent's NFT state, **then mints one NFT** to the block owner. State entries contain owner, sequence (initially 0), minted height and page ID. Transactions cannot spend this block's new issuance.
|
||||
|
||||
A transfer carries network, chain ID, `cardHash` (**NFT token ID**, despite the legacy field name), from, to, sequence, public key and signature. Its signed message is `J([NETWORK, 'nft-transfer', chainId, cardHash, from, to, sequence])`. **Transfer ID is SHA-256 of that message, excluding ECDSA signature bytes** to avoid signature-malleability identity changes. Require the current owner, matching public key, a different recipient, exact current sequence, valid signature, and inclusion height minus mint height **>= 2**; increment sequence on application. Replays and duplicate IDs within a block fail.
|
||||
An individual transfer carries network, chain ID, `cardHash` (**NFT token ID**, despite the legacy field name), from, to, sequence, public key and signature. Its signed message is `J([NETWORK, 'nft-transfer', chainId, cardHash, from, to, sequence])`. **Transfer ID is SHA-256 of that message, excluding ECDSA signature bytes** to avoid signature-malleability identity changes. Require the current owner, matching public key, a different recipient, exact current sequence, valid signature, and inclusion height minus mint height **>= 2**; increment sequence on application. Replays and duplicate IDs within a block fail.
|
||||
|
||||
`transactionsRoot` commits the **ordered transfer IDs**. `stateRoot` commits the complete **post-transfer, post-mint** state: sort token IDs lexicographically; each input leaf is `S(J([id, owner, sequence, mintedHeight, pageId]))`. `merkleRoot` returns `S(NETWORK + ':empty')` for no leaves. For nonempty input, hash each raw 32-byte input leaf with byte prefix `0x00`; hash pairs of raw children with byte prefix `0x01`, duplicating an odd final child at each layer. Finally return `S(J([NETWORK, 'merkle', originalLeafCount, treeRootHex]))`. Count commitment prevents odd-padding ambiguity; leaf/node prefixes separate domains. These commitment semantics, the RandomX version, and big-endian proof convention are pinned in the genesis consensus manifest.
|
||||
A trade is one `type: 'trade'` transaction with proposer and counterparty addresses, ordered offered/requested NFT legs (token ID plus current sequence), and one P-256 signature from each party over `J([NETWORK, 'nft-trade', chainId, proposer, counterparty, offeredLegs, requestedLegs])`, where each leg is `[cardHash, sequence]`. Both public keys must derive the respective party addresses. Each side must contain 1–8 distinct NFTs; duplicates across either side are rejected. At inclusion, validation checks **all** selected NFTs against the same pre-trade state, owners, sequences, maturity, and both signatures before writing any changes. If any leg is invalid, the entire trade is rejected; if valid, all offered ownerships move to the counterparty and all requested ownerships to the proposer, with each sequence incremented once. A trade counts as one mempool and block transaction and is committed by one transaction ID. It cannot be partially included or partially replayed. Offers are not reservations; stale offers fail validation.
|
||||
|
||||
The mempool holds at most **256** ordered transfers, validating sequentially against the next inclusion height; a template takes the first **64**. The protocol can accept chained transfers of the same NFT in order; the browser conservatively blocks a second pending transfer for a card. A 202 response changes **no canonical ownership**. Mint at height `h` is eligible for inclusion at `h+2` (pool admission can happen at tip `h+1`). Confirmations are `tipHeight - mintedHeight + 1`, not a finality guarantee.
|
||||
`transactionsRoot` commits the **ordered transaction IDs** (individual transfer IDs or atomic trade IDs). `stateRoot` commits the complete **post-transaction, post-mint** state: sort token IDs lexicographically; each input leaf is `S(J([id, owner, sequence, mintedHeight, pageId]))`. `merkleRoot` returns `S(NETWORK + ':empty')` for no leaves. For nonempty input, hash each raw 32-byte input leaf with byte prefix `0x00`; hash pairs of raw children with byte prefix `0x01`, duplicating an odd final child at each layer. Finally return `S(J([NETWORK, 'merkle', originalLeafCount, treeRootHex]))`. Count commitment prevents odd-padding ambiguity; leaf/node prefixes separate domains. These commitment semantics, the RandomX version, and big-endian proof convention are pinned in the genesis consensus manifest.
|
||||
|
||||
The mempool holds at most **256** ordered transactions (transfers or trade bundles), validating sequentially against the next inclusion height; a template takes the first **64 transaction units**. A 202 response changes **no canonical ownership**. Mint at height `h` is eligible for inclusion at `h+2` (pool admission can happen at tip `h+1`). Confirmations are `tipHeight - mintedHeight + 1`, not a finality guarantee.
|
||||
|
||||
## Fork choice, reorgs and storage
|
||||
|
||||
Per-block work is `floor(2^256 / (target + 1))`; accumulate from genesis work zero. Keep all valid known-parent branches. Adopt a tip only for **strictly greater cumulative work**; equal-work ties retain the current tip, including across replay of persisted insertion order. A shorter, harder branch can win.
|
||||
|
||||
On reorg, reconstruct ownership from the winning branch. Orphan mints disappear from canonical ownership; disconnected transfers are reconsidered before existing pending entries, in order, only if still valid and not already committed. Conflicting, immature or orphan-issuance transfers are discarded. No checkpoints/finality or rollback protection beyond observed work; an isolated node has no peer-backed security.
|
||||
On reorg, reconstruct ownership from the winning branch. Orphan mints disappear from canonical ownership; disconnected transactions are reconsidered before existing pending entries, in order, only if still valid and not already committed. A trade is requeued as one unit only when every leg remains valid; otherwise the complete bundle is discarded. No checkpoints/finality or rollback protection beyond observed work; an isolated node has no peer-backed security.
|
||||
|
||||
The [ledger](../server/testnet/ledger.ts) is stored in `data/testnet/ledger.sqlite`, under the `WIKIDECK_DATA_DIR` testnet subdirectory (Docker uses `/app/data`). SQLite WAL mode with full synchronous commits stores each accepted block once in an indexed table and replaces the bounded mempool in the same transaction; writes no longer serialize/rewrite the entire DAG, and there is no 128 MiB/10,000-block ledger cap. Existing v5 `ledger.json` files are validated and imported transactionally once; the source JSON is left untouched as a recovery copy. Back up the complete stopped-node data directory, including SQLite database/WAL files. Do not copy only `ledger.sqlite` while the node is running.
|
||||
The [ledger](../server/testnet/ledger.ts) is stored in `data/testnet/ledger.sqlite`, under the `WIKIDECK_DATA_DIR` testnet subdirectory (Docker uses `/app/data`). SQLite WAL mode with full synchronous commits stores each accepted block once in an indexed table and replaces the bounded mempool in the same transaction; writes no longer serialize/rewrite the entire DAG, and there is no 128 MiB/10,000-block ledger cap. A matching v6 legacy `ledger.json` is schema-checked and imported transactionally once; the source JSON is left untouched as a recovery copy. Earlier-protocol ledgers are not compatible and fail closed. Back up the complete stopped-node data directory, including SQLite database/WAL files. Do not copy only `ledger.sqlite` while the node is running.
|
||||
|
||||
The in-memory DAG keeps block records and cumulative work, while each block's NFT state is a persistent overlay containing only that block's changes rather than a full state copy. State-root calculation still visits the complete NFT state, and startup still revalidates every stored proof; CPU and startup work therefore grow with chain size. This is materially more storage-efficient, but not yet an archival-scale chain database or a public DoS defense. Blocks remain individually limited to **64 KiB and 64 transfers**; the mempool remains capped at **256 transfers**. Mutations are serialized (queue cap 256). HTTP startup exclusively creates a data-directory lock named `testnet-node.lock`. It is removed on normal shutdown, **not automatically after a crash**: stop and verify all owning processes have exited before manual stale-lock removal. Never bypass a live lock or share a writer directory. Back up node data and wallet exports separately; deleting a volume cannot be recovered from a wallet alone.
|
||||
The in-memory DAG keeps block records and cumulative work, while each block's NFT state is a persistent overlay containing only that block's changes rather than a full state copy. State-root calculation still visits the complete NFT state, and startup still revalidates every stored proof; CPU and startup work therefore grow with chain size. This is materially more storage-efficient, but not yet an archival-scale chain database or a public DoS defense. Blocks remain individually limited to **64 KiB and 64 transaction units**; the mempool remains capped at **256 transaction units**. Mutations are serialized (queue cap 256). HTTP startup exclusively creates a data-directory lock named `testnet-node.lock`. It is removed on normal shutdown, **not automatically after a crash**: stop and verify all owning processes have exited before manual stale-lock removal. Never bypass a live lock or share a writer directory. Back up node data and wallet exports separately; deleting a volume cannot be recovered from a wallet alone.
|
||||
|
||||
## Fixed peers and HTTP surface
|
||||
|
||||
Peers are **explicit operator-configured URL origins**, up to 16, not discovered from network input. Default: HTTPS/public addresses only, checked and pinned during DNS lookup; no credentials, paths, queries, fragments or redirects. `WIKIDECK_ALLOW_PRIVATE_PEERS=1` permits private/HTTP destinations only by explicit operator choice (used for local Docker tests). No bootnodes, DHT, automatic public deployment or claimed Sybil resistance.
|
||||
|
||||
Sync polls every **5 seconds**, checks network/chain ID, imports at most **32** topologically ordered DAG blocks per peer/pass, and validates remote pool entries locally. Accepted blocks/transfers are gossiped to available configured peers; busy-peer gossip is dropped, with polling for catch-up. Responses are capped at 2 MiB with a 10-second deadline. Peer count is current outbound connectivity metadata, not consensus membership or a security score.
|
||||
Sync polls every **5 seconds**, checks network/chain ID, imports at most **32** topologically ordered DAG blocks per peer/pass, and validates remote pool entries locally. Accepted blocks and transactions (including complete trade bundles) are gossiped to configured peers; busy-peer gossip is dropped, with polling for catch-up. Responses are capped at 2 MiB with a 10-second deadline. Peer count is current outbound connectivity metadata, not consensus membership or a security score.
|
||||
|
||||
| Route | Purpose |
|
||||
| --- | --- |
|
||||
| `GET /healthz` | Process health/network, not sync or consensus security. |
|
||||
| `GET /api/state`, `/api/chain` | Canonical blocks, mined/pending transfers, tip/work and peer count. |
|
||||
| `GET /api/state`, `/api/chain` | Canonical blocks, mined/pending transactions, tip/work and peer count. |
|
||||
| `GET /api/nfts/:id`, `/api/peers` | Canonical NFT owner/sequence/confirmations, article and at-mint statistics; configured peer status. |
|
||||
| `GET /api/decks/:address?offset=0&limit=24` | Public canonical deck for a wallet address at one tip; paged, maximum 24 cards per response. This reveals public address holdings. |
|
||||
| `GET /api/wikipedia/:pageId` | Cached current enrichment for positive English Wikipedia page IDs: categories, views, size, languages and display-only scores. Never a consensus input. |
|
||||
| `POST /api/mining/template` | Rate-limited public miner identity; fetches a random article with a picture plus fresh at-mint statistics. Returns 503 when either cannot be fetched. |
|
||||
| `POST /api/blocks` | Independently validates a signed block, proof, article snapshot, required Wikimedia image URL, and statistics snapshot consistency. Repeated page IDs are accepted. |
|
||||
| `POST /api/transfer` | Signed transfer admission; 202 means pending. |
|
||||
| `POST /api/transfer` | Signed single-card transfer admission; 202 means pending. |
|
||||
| `POST /api/trades` | Validates and admits one signed, atomic multi-card trade bundle; 202 means the whole bundle is pending. |
|
||||
| `GET /p2p/status`, `/p2p/blocks?offset=0`, `/p2p/mempool` | Manifest/size/tip, 32-block inventory pages, pending transactions. |
|
||||
| `POST /p2p/block`, `/p2p/transaction` | Same validation as local block/transfer submission. |
|
||||
| `POST /p2p/block`, `/p2p/transaction` | Same validation as local block/transaction submission; transaction endpoint accepts a transfer or atomic trade. |
|
||||
|
||||
POSTs require JSON, enforce supplied browser origins and signatures where applicable; peers without an Origin header are not authenticated by this check. HTTP parsing is capped at 68 KiB; API/P2P rate limits are 300/minute and block/transfer POSTs additionally 30/minute. There are no fees, peer authentication or economic spam deterrents. Same-origin browser RPC trusts the node; it does not independently verify these commitments. Deploy no funds: majority-hashpower, eclipse, censorship, reorg and resource-exhaustion risks require independent audits and adversarial testing before any broader launch.
|
||||
POSTs require JSON, enforce supplied browser origins and signatures where applicable; peers without an Origin header are not authenticated by this check. HTTP parsing is capped at 68 KiB; API/P2P rate limits are 300/minute and block/transaction/trade submissions additionally 30/minute. There are no fees, peer authentication or economic spam deterrents. Same-origin browser RPC trusts the node; it does not independently verify these commitments. Deploy no funds: majority-hashpower, eclipse, censorship, reorg and resource-exhaustion risks require independent audits and adversarial testing before any broader launch.
|
||||
|
||||
Reference in New Issue
Block a user