Files
dchain/blockchain/types.go
vsecoder 7e7393e4f8 chore: initial commit for v0.0.1
DChain single-node blockchain + React Native messenger client.

Core:
- PBFT consensus with multi-sig validator admission + equivocation slashing
- BadgerDB + schema migration scaffold (CurrentSchemaVersion=0)
- libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1)
- Native Go contracts (username_registry) alongside WASM (wazero)
- WebSocket gateway with topic-based fanout + Ed25519-nonce auth
- Relay mailbox with NaCl envelope encryption (X25519 + Ed25519)
- Prometheus /metrics, per-IP rate limit, body-size cap

Deployment:
- Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus
- 3-node dev compose (docker-compose.yml) with mocked internet topology
- 3-validator prod compose (deploy/prod/) for federation
- Auto-update from Gitea via /api/update-check + systemd timer
- Build-time version injection (ldflags → node --version)
- UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER)

Client (client-app/):
- Expo / React Native / NativeWind
- E2E NaCl encryption, typing indicator, contact requests
- Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch

Documentation:
- README.md, CHANGELOG.md, CONTEXT.md
- deploy/single/README.md with 6 operator scenarios
- deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design
- docs/contracts/*.md per contract
2026-04-17 14:16:44 +03:00

510 lines
21 KiB
Go

package blockchain
import (
"crypto/sha256"
"encoding/binary"
"time"
)
// EventType defines what kind of event a transaction represents.
type EventType string
const (
EventRegisterKey EventType = "REGISTER_KEY"
EventCreateChannel EventType = "CREATE_CHANNEL"
EventAddMember EventType = "ADD_MEMBER"
EventOpenPayChan EventType = "OPEN_PAY_CHAN"
EventClosePayChan EventType = "CLOSE_PAY_CHAN"
EventTransfer EventType = "TRANSFER"
EventRelayProof EventType = "RELAY_PROOF"
EventRegisterRelay EventType = "REGISTER_RELAY" // node advertises relay service
EventBindWallet EventType = "BIND_WALLET" // node binds a payout wallet address
EventSlash EventType = "SLASH" // penalise a misbehaving validator
EventHeartbeat EventType = "HEARTBEAT" // liveness ping from a node
EventBlockReward EventType = "BLOCK_REWARD" // synthetic tx indexed on block commit
EventContactRequest EventType = "CONTACT_REQUEST" // paid first-contact request (ICQ-style)
EventAcceptContact EventType = "ACCEPT_CONTACT" // recipient accepts a pending request
EventBlockContact EventType = "BLOCK_CONTACT" // recipient blocks a sender
EventAddValidator EventType = "ADD_VALIDATOR" // existing validator adds a new one
EventRemoveValidator EventType = "REMOVE_VALIDATOR" // existing validator removes one (or self-removal)
EventDeployContract EventType = "DEPLOY_CONTRACT" // deploy a WASM smart contract
EventCallContract EventType = "CALL_CONTRACT" // call a method on a deployed contract
EventStake EventType = "STAKE" // lock tokens as validator stake
EventUnstake EventType = "UNSTAKE" // release staked tokens back to balance
EventIssueToken EventType = "ISSUE_TOKEN" // create a new fungible token
EventTransferToken EventType = "TRANSFER_TOKEN" // transfer fungible tokens between addresses
EventBurnToken EventType = "BURN_TOKEN" // destroy fungible tokens
EventMintNFT EventType = "MINT_NFT" // mint a new non-fungible token
EventTransferNFT EventType = "TRANSFER_NFT" // transfer NFT ownership
EventBurnNFT EventType = "BURN_NFT" // burn (destroy) an NFT
)
// Token amounts are stored in micro-tokens (µT).
// 1 token = 1_000_000 µT
const (
MicroToken uint64 = 1
Token uint64 = 1_000_000
// MinFee is the minimum transaction fee paid to the block validator.
// Validators earn fees as their only income — no block reward minting.
MinFee uint64 = 1_000 // 0.001 T per transaction
// GenesisAllocation is a one-time mint at block 0 for the bootstrap validator.
// All subsequent token supply comes only from re-distribution of existing balances.
GenesisAllocation uint64 = 21_000_000 * Token // 21 million T, fixed supply
// SlashAmount is the penalty deducted from a misbehaving validator's balance.
SlashAmount uint64 = 50 * Token
// RegistrationFee is the one-time fee to register an identity on-chain
// (EventRegisterKey). Paid to the block validator. High enough to deter
// Sybil attacks while remaining affordable.
RegistrationFee uint64 = 1_000_000 // 1 T
// MinContactFee is the minimum amount a sender must pay the recipient when
// submitting an EventContactRequest (anti-spam; goes directly to recipient).
MinContactFee uint64 = 5_000 // 0.005 T
)
// Transaction is the atomic unit recorded in a block.
// Bodies of messages are NEVER stored here — only identity/channel events.
type Transaction struct {
ID string `json:"id"`
Type EventType `json:"type"`
From string `json:"from"` // hex-encoded Ed25519 public key
To string `json:"to"` // hex-encoded Ed25519 public key (if applicable)
Amount uint64 `json:"amount"` // µT to transfer (for TRANSFER type)
Fee uint64 `json:"fee"` // µT paid to the block validator
Memo string `json:"memo,omitempty"`
Payload []byte `json:"payload"` // JSON-encoded event-specific data
Signature []byte `json:"signature"` // Ed25519 sig over canonical bytes
Timestamp time.Time `json:"timestamp"`
}
// RegisterKeyPayload is embedded in EventRegisterKey transactions.
type RegisterKeyPayload struct {
PubKey string `json:"pub_key"` // hex-encoded Ed25519 public key
Nickname string `json:"nickname"` // human-readable, non-unique
PowNonce uint64 `json:"pow_nonce"` // proof-of-work nonce (Sybil barrier)
PowTarget string `json:"pow_target"`
X25519PubKey string `json:"x25519_pub_key,omitempty"` // hex Curve25519 key for E2E messaging
}
// CreateChannelPayload is embedded in EventCreateChannel transactions.
type CreateChannelPayload struct {
ChannelID string `json:"channel_id"`
Title string `json:"title"`
IsPublic bool `json:"is_public"`
}
// RegisterRelayPayload is embedded in EventRegisterRelay transactions.
// A node publishes this to advertise itself as a relay service provider.
// Clients look up relay nodes via GET /api/relays.
type RegisterRelayPayload struct {
// X25519PubKey is the hex-encoded Curve25519 public key for NaCl envelope encryption.
// Senders use this key to seal messages addressed to this relay node.
X25519PubKey string `json:"x25519_pub_key"`
// FeePerMsgUT is the relay fee the node charges per delivered envelope (in µT).
FeePerMsgUT uint64 `json:"fee_per_msg_ut"`
// Multiaddr is the optional libp2p multiaddr string for direct connections.
Multiaddr string `json:"multiaddr,omitempty"`
}
// RelayProofPayload proves that a relay/recipient node received an envelope.
// The sender pre-authorises the fee by signing FeeAuthBytes(EnvelopeID, FeeUT).
// On-chain the fee is pulled from the sender's balance and credited to the relay.
type RelayProofPayload struct {
// EnvelopeID is the stable identifier of the delivered envelope (hex).
EnvelopeID string `json:"envelope_id"`
// EnvelopeHash is SHA-256(nonce || ciphertext) — prevents double-claiming.
EnvelopeHash []byte `json:"envelope_hash"`
// SenderPubKey is the Ed25519 public key of the envelope sender (hex).
SenderPubKey string `json:"sender_pub_key"`
// FeeUT is the delivery fee the relay claims from the sender's balance.
FeeUT uint64 `json:"fee_ut"`
// FeeSig is the sender's Ed25519 signature over FeeAuthBytes(EnvelopeID, FeeUT).
// This authorises the relay to pull FeeUT from the sender's on-chain balance.
FeeSig []byte `json:"fee_sig"`
// RelayPubKey is the Ed25519 public key of the relay claiming the fee (hex).
RelayPubKey string `json:"relay_pub_key"`
// DeliveredAt is the unix timestamp of delivery.
DeliveredAt int64 `json:"delivered_at"`
// RecipientSig is the recipient's optional Ed25519 sig over EnvelopeHash,
// proving the message was successfully decrypted (not required for fee claim).
RecipientSig []byte `json:"recipient_sig,omitempty"`
}
// FeeAuthBytes returns the canonical byte string that the sender must sign
// to pre-authorise a relay fee pull. The relay includes this signature in
// RelayProofPayload.FeeSig when submitting the proof on-chain.
//
// Format: SHA-256("relay-fee:" || envelopeID || uint64BE(feeUT))
func FeeAuthBytes(envelopeID string, feeUT uint64) []byte {
h := sha256.New()
h.Write([]byte("relay-fee:"))
h.Write([]byte(envelopeID))
var b [8]byte
binary.BigEndian.PutUint64(b[:], feeUT)
h.Write(b[:])
return h.Sum(nil)
}
// TransferPayload carries an optional memo for token transfers.
type TransferPayload struct {
Memo string `json:"memo,omitempty"`
}
// BindWalletPayload links a node's signing key to a separate payout wallet.
// After this tx is committed, block fees and relay fees are credited to
// WalletPubKey instead of the node's own pub key.
type BindWalletPayload struct {
WalletPubKey string `json:"wallet_pub_key"`
WalletAddr string `json:"wallet_addr"`
}
// SlashPayload is submitted by a validator to penalise a misbehaving peer.
type SlashPayload struct {
OffenderPubKey string `json:"offender_pub_key"`
Reason string `json:"reason"` // "double_vote" | "downtime" | "equivocation"
Evidence []byte `json:"evidence,omitempty"`
}
// HeartbeatPayload is a periodic liveness signal published by active nodes.
// It carries the node's current chain height so peers can detect lagging nodes.
// Heartbeats cost MinFee (paid to the block validator) and earn no reward —
// they exist to build reputation and prove liveness.
type HeartbeatPayload struct {
PubKey string `json:"pub_key"`
ChainHeight uint64 `json:"chain_height"`
PeerCount int `json:"peer_count"`
Version string `json:"version"`
}
// OpenPayChanPayload locks deposits from two parties into a payment channel.
type OpenPayChanPayload struct {
ChannelID string `json:"channel_id"`
PartyA string `json:"party_a"`
PartyB string `json:"party_b"`
DepositA uint64 `json:"deposit_a_ut"`
DepositB uint64 `json:"deposit_b_ut"`
ExpiryBlock uint64 `json:"expiry_block"`
SigB []byte `json:"sig_b"` // PartyB's Ed25519 sig over channel params
}
// ClosePayChanPayload settles a payment channel and distributes balances.
type ClosePayChanPayload struct {
ChannelID string `json:"channel_id"`
BalanceA uint64 `json:"balance_a_ut"`
BalanceB uint64 `json:"balance_b_ut"`
Nonce uint64 `json:"nonce"`
SigA []byte `json:"sig_a"`
SigB []byte `json:"sig_b"`
}
// PayChanState is stored on-chain for each open payment channel.
type PayChanState struct {
ChannelID string `json:"channel_id"`
PartyA string `json:"party_a"`
PartyB string `json:"party_b"`
DepositA uint64 `json:"deposit_a_ut"`
DepositB uint64 `json:"deposit_b_ut"`
ExpiryBlock uint64 `json:"expiry_block"`
OpenedBlock uint64 `json:"opened_block"`
Nonce uint64 `json:"nonce"`
Closed bool `json:"closed"`
}
// BlockRewardPayload is attached to synthetic BLOCK_REWARD transactions.
// These are index-only records so the explorer can show validator fee income.
// There is no minting — the FeeReward comes from existing transaction fees.
type BlockRewardPayload struct {
ValidatorPubKey string `json:"validator_pub_key"`
TargetPubKey string `json:"target_pub_key"`
FeeReward uint64 `json:"fee_reward_ut"`
TotalReward uint64 `json:"total_reward_ut"`
}
// ContactRequestPayload is embedded in EventContactRequest transactions.
// The sender pays tx.Amount directly to the recipient (anti-spam fee).
// A pending contact record is stored on-chain for the recipient to accept or block.
type ContactRequestPayload struct {
Intro string `json:"intro,omitempty"` // optional plaintext intro (≤ 280 chars)
}
// AcceptContactPayload is embedded in EventAcceptContact transactions.
// tx.From accepts a pending request from tx.To.
type AcceptContactPayload struct{}
// BlockContactPayload is embedded in EventBlockContact transactions.
// tx.From blocks tx.To; future contact requests from tx.To are rejected.
type BlockContactPayload struct {
Reason string `json:"reason,omitempty"`
}
// ChannelMember records a participant in a channel together with their
// X25519 public key. The key is cached on-chain (written during ADD_MEMBER)
// so channel senders don't have to fan out a separate /api/identity lookup
// per recipient on every message — they GET /api/channels/:id/members
// once and seal N envelopes in a loop.
type ChannelMember struct {
PubKey string `json:"pub_key"` // Ed25519 hex
X25519PubKey string `json:"x25519_pub_key"` // optional; empty if member hasn't registered
Address string `json:"address"`
}
// AddMemberPayload is embedded in EventAddMember transactions.
// tx.From adds tx.To as a member of the specified channel.
// If tx.To is empty, tx.From is added (self-join for public channels).
type AddMemberPayload struct {
ChannelID string `json:"channel_id"`
}
// AddValidatorPayload is embedded in EventAddValidator transactions.
// tx.From must already be a validator; tx.To is the new validator's pub key.
//
// Admission is gated by two things:
// 1. Stake: the candidate (tx.To) must have STAKE'd at least
// MinValidatorStake beforehand. Prevents anyone spinning up a free
// validator without economic buy-in.
// 2. Multi-sig: at least ⌈2/3⌉ of the CURRENT validator set must approve.
// The tx sender counts as one; remaining approvals go in CoSignatures.
// For a 1-validator chain (fresh genesis / tests) sender alone is 2/3,
// so CoSignatures can be empty — backward-compat is preserved.
type AddValidatorPayload struct {
Reason string `json:"reason,omitempty"`
CoSignatures []ValidatorCoSig `json:"cosigs,omitempty"`
}
// ValidatorCoSig is an off-chain-assembled approval from one existing
// validator for a specific candidate admission. The signature is over the
// canonical digest returned by AdmitDigest(candidatePubKeyHex).
type ValidatorCoSig struct {
PubKey string `json:"pubkey"` // Ed25519 hex of a current validator
Signature []byte `json:"signature"` // Ed25519 signature over AdmitDigest(candidate)
}
// AdmitDigest returns the canonical bytes a validator signs to approve
// admitting `candidatePubHex` as a new validator. Stable across implementations
// so co-sigs collected off-chain verify identically on-chain.
func AdmitDigest(candidatePubHex string) []byte {
h := sha256.New()
h.Write([]byte("DCHAIN-ADD-VALIDATOR\x00"))
h.Write([]byte(candidatePubHex))
return h.Sum(nil)
}
// MinValidatorStake is the minimum µT a candidate must have locked in
// `stake:<pubkey>` before an ADD_VALIDATOR naming them is accepted.
// 1 T = 1_000_000 µT — small enough that testnets can afford it easily,
// large enough to deter "register 100 fake validators to 51%-attack".
const MinValidatorStake uint64 = 1_000_000
// RemoveValidatorPayload is embedded in EventRemoveValidator transactions.
// tx.From must be a validator; tx.To is the validator to remove.
//
// Two legitimate use cases:
// 1. Self-removal (tx.From == tx.To): always allowed, no cosigs needed.
// Lets a validator gracefully leave the set without requiring others.
// 2. Forced removal (tx.From != tx.To): requires ⌈2/3⌉ cosigs of the
// current validator set — same pattern as ADD_VALIDATOR. Stops a
// single validator from unilaterally kicking peers.
//
// The signed payload is AdmitDigest(tx.To) but with the domain byte flipped
// — see RemoveDigest below. This prevents a cosig collected for "admit X"
// from being replayed as "remove X".
type RemoveValidatorPayload struct {
Reason string `json:"reason,omitempty"`
CoSignatures []ValidatorCoSig `json:"cosigs,omitempty"`
}
// RemoveDigest is the canonical bytes a validator signs to approve removing
// `targetPubHex` from the set. Distinct from AdmitDigest so signatures
// can't be cross-replayed between add and remove operations.
func RemoveDigest(targetPubHex string) []byte {
h := sha256.New()
h.Write([]byte("DCHAIN-REMOVE-VALIDATOR\x00"))
h.Write([]byte(targetPubHex))
return h.Sum(nil)
}
// DeployContractPayload is embedded in EventDeployContract transactions.
// WASMBase64 is the base64-encoded WASM binary. It is stored in the tx so that
// nodes can replay the chain from genesis and re-derive contract state.
type DeployContractPayload struct {
WASMBase64 string `json:"wasm_b64"`
ABIJson string `json:"abi_json"`
InitArgs string `json:"init_args_json,omitempty"`
}
// CallContractPayload is embedded in EventCallContract transactions.
type CallContractPayload struct {
ContractID string `json:"contract_id"`
Method string `json:"method"`
ArgsJSON string `json:"args_json,omitempty"`
GasLimit uint64 `json:"gas_limit"`
}
// ContractRecord is stored in BadgerDB at contract:<contractID>.
// WASMBytes is NOT in the block; it is derived from the deploy tx payload on replay.
type ContractRecord struct {
ContractID string `json:"contract_id"`
WASMBytes []byte `json:"wasm_bytes"`
ABIJson string `json:"abi_json"`
DeployerPub string `json:"deployer_pub"`
DeployedAt uint64 `json:"deployed_at"` // block height
}
// MinDeployFee is the minimum fee for a DEPLOY_CONTRACT transaction.
// Covers storage costs for the WASM binary.
const MinDeployFee uint64 = 10_000 // 0.01 T
// MinCallFee is the minimum base fee for a CALL_CONTRACT transaction.
// Gas costs are billed on top of this.
const MinCallFee uint64 = MinFee
// ContractLogEntry is one log message emitted by a contract via env.log().
// Stored in BadgerDB at clog:<contractID>:<blockHeight_20d>:<seq_05d>.
type ContractLogEntry struct {
ContractID string `json:"contract_id"`
BlockHeight uint64 `json:"block_height"`
TxID string `json:"tx_id"`
Seq int `json:"seq"`
Message string `json:"message"`
}
// GasPrice is the cost in µT per 1 gas unit consumed during contract execution.
const GasPrice uint64 = 1 // 1 µT per gas unit
// MinStake is the minimum amount a validator must stake.
const MinStake uint64 = 1_000 * Token // 1000 T
// MinIssueTokenFee is the fee required to issue a new token.
const MinIssueTokenFee uint64 = 100_000 // 0.1 T
// StakePayload is embedded in EventStake transactions.
// tx.Amount holds the amount to stake; tx.Fee is the transaction fee.
type StakePayload struct{}
// UnstakePayload is embedded in EventUnstake transactions.
// The entire current stake is returned to the staker's balance.
type UnstakePayload struct{}
// IssueTokenPayload is embedded in EventIssueToken transactions.
// The new token is credited to tx.From with TotalSupply units.
type IssueTokenPayload struct {
Name string `json:"name"` // human-readable token name, e.g. "My Token"
Symbol string `json:"symbol"` // ticker symbol, e.g. "MTK"
Decimals uint8 `json:"decimals"` // decimal places, e.g. 6 → 1 token = 1_000_000 base units
TotalSupply uint64 `json:"total_supply"` // initial supply in base units
}
// TransferTokenPayload is embedded in EventTransferToken transactions.
// tx.To is the recipient; tx.Amount is ignored (use payload Amount).
type TransferTokenPayload struct {
TokenID string `json:"token_id"`
Amount uint64 `json:"amount"` // in base units
}
// BurnTokenPayload is embedded in EventBurnToken transactions.
type BurnTokenPayload struct {
TokenID string `json:"token_id"`
Amount uint64 `json:"amount"` // in base units
}
// TokenRecord is stored in BadgerDB at token:<tokenID>.
type TokenRecord struct {
TokenID string `json:"token_id"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals uint8 `json:"decimals"`
TotalSupply uint64 `json:"total_supply"` // current (may decrease via burns)
Issuer string `json:"issuer"` // creator pubkey
IssuedAt uint64 `json:"issued_at"` // block height
}
// MinMintNFTFee is the fee required to mint a new NFT.
const MinMintNFTFee uint64 = 10_000 // 0.01 T
// MintNFTPayload is embedded in EventMintNFT transactions.
type MintNFTPayload struct {
Name string `json:"name"` // human-readable name
Description string `json:"description,omitempty"`
URI string `json:"uri,omitempty"` // off-chain metadata URI (IPFS, https, etc.)
Attributes string `json:"attributes,omitempty"` // JSON string of trait attributes
}
// TransferNFTPayload is embedded in EventTransferNFT transactions.
// tx.To is the new owner; tx.From must be current owner.
type TransferNFTPayload struct {
NFTID string `json:"nft_id"`
}
// BurnNFTPayload is embedded in EventBurnNFT transactions.
type BurnNFTPayload struct {
NFTID string `json:"nft_id"`
}
// NFTRecord is stored in BadgerDB at nft:<nftID>.
type NFTRecord struct {
NFTID string `json:"nft_id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
URI string `json:"uri,omitempty"`
Attributes string `json:"attributes,omitempty"`
Owner string `json:"owner"` // current owner pubkey
Issuer string `json:"issuer"` // original minter pubkey
MintedAt uint64 `json:"minted_at"` // block height
Burned bool `json:"burned,omitempty"`
}
// ContactStatus is the state of a contact relationship.
type ContactStatus string
const (
ContactPending ContactStatus = "pending"
ContactAccepted ContactStatus = "accepted"
ContactBlocked ContactStatus = "blocked"
)
// ContactInfo is returned by the contacts API.
type ContactInfo struct {
RequesterPub string `json:"requester_pub"`
RequesterAddr string `json:"requester_addr"`
Status ContactStatus `json:"status"`
Intro string `json:"intro,omitempty"`
FeeUT uint64 `json:"fee_ut"`
TxID string `json:"tx_id"`
CreatedAt int64 `json:"created_at"`
}
// IdentityInfo is returned by GET /api/identity/{pubkey}.
type IdentityInfo struct {
PubKey string `json:"pub_key"`
Address string `json:"address"`
X25519Pub string `json:"x25519_pub"` // hex Curve25519 key; empty if not published
Nickname string `json:"nickname"`
Registered bool `json:"registered"` // true if REGISTER_KEY tx was committed
}
// ConsensusMessage types used by the PBFT engine over the P2P layer.
type MsgType string
const (
MsgPrePrepare MsgType = "PRE_PREPARE"
MsgPrepare MsgType = "PREPARE"
MsgCommit MsgType = "COMMIT"
MsgViewChange MsgType = "VIEW_CHANGE"
MsgNewView MsgType = "NEW_VIEW"
)
// ConsensusMsg is the envelope sent between validators.
type ConsensusMsg struct {
Type MsgType `json:"type"`
View uint64 `json:"view"`
SeqNum uint64 `json:"seq_num"`
BlockHash []byte `json:"block_hash"`
Block *Block `json:"block,omitempty"`
From string `json:"from"`
Signature []byte `json:"signature"`
}