Replaces the channel/membership model with a VK/Twitter-style feed:
public posts, follow graph, likes. Views are deliberately off-chain
(counted by the hosting relay, Phase B).
Removed
- EventCreateChannel, EventAddMember
- CreateChannelPayload, AddMemberPayload, ChannelMember
- prefixChannel, prefixChanMember
- chain.Channel(), chain.ChannelMembers()
- node/api_channels.go
- GetChannel, GetChannelMembers on ExplorerQuery
Added
- Events: CREATE_POST, DELETE_POST, FOLLOW, UNFOLLOW, LIKE_POST, UNLIKE_POST
- Payloads: CreatePostPayload, DeletePostPayload, FollowPayload,
UnfollowPayload, LikePostPayload, UnlikePostPayload
- Stored shape: PostRecord (author, size, hash, hosting relay, timestamp,
reply/quote refs, soft-delete flag, fee paid)
- State prefixes: post:, postbyauthor:, follow:, followin:, like:, likecount:
- Queries: Post(), PostsByAuthor(), Following(), Followers(),
LikeCount(), HasLiked()
- Cached like counter via bumpLikeCount helper
Pricing
- BasePostFee = 1000 µT (aligned with MinFee block-validation floor)
- PostByteFee = 1 µT/byte of compressed content
- Total fee credited in full to HostingRelay pub (storage compensation)
- MaxPostSize = 256 KiB
Integrity
- CREATE_POST validates content_hash length (32 B) and size range
- DELETE_POST restricted to post.Author
- Duplicate FOLLOW / LIKE rejected
- reply_to and quote_of mutually exclusive
Tests
- TestFeedCreatePost: post stored, indexed, host credited
- TestFeedInsufficientFee: underpaid post is skipped
- TestFeedFollowUnfollow: follow graph round-trips via forward + inbound indices
- TestFeedLikeUnlike: like toggles with dedup, counter stays accurate
- TestFeedDeletePostByOther: non-author deletion rejected
This is Phase A (chain-layer). Phase B adds the relay feed-mailbox
(post bodies + gossipsub) and HTTP endpoints. Phase C adds the client
Feed tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
581 lines
25 KiB
Go
581 lines
25 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"
|
||
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
|
||
|
||
// ── Social feed (v2.0.0) ──────────────────────────────────────────────
|
||
// Replaces the old channel model with a VK/Twitter-style timeline.
|
||
// Posts are plaintext, publicly readable, size-priced. Bodies live in
|
||
// the relay feed-mailbox; on-chain we only keep metadata + author.
|
||
EventCreatePost EventType = "CREATE_POST" // author publishes a post
|
||
EventDeletePost EventType = "DELETE_POST" // author soft-deletes their post
|
||
EventFollow EventType = "FOLLOW" // follow another author's feed
|
||
EventUnfollow EventType = "UNFOLLOW" // unfollow an author
|
||
EventLikePost EventType = "LIKE_POST" // like a post
|
||
EventUnlikePost EventType = "UNLIKE_POST" // remove a previous like
|
||
)
|
||
|
||
// 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
|
||
|
||
// ── Feed pricing (v2.0.0) ─────────────────────────────────────────────
|
||
// A post's on-chain fee is BasePostFee + bytes_on_disk × PostByteFee.
|
||
// The fee is paid by the author and credited in full to the hosting
|
||
// relay (the node that received POST /feed/publish and stored the body).
|
||
// Size-based pricing is what aligns incentives: a 200-byte tweet is
|
||
// cheap, a 256 KB video costs ~0.26 T — node operators' storage cost
|
||
// is covered.
|
||
//
|
||
// Note: BasePostFee is set to MinFee (1000 µT) because chain-level block
|
||
// validation requires every tx's Fee ≥ MinFee. So the true minimum a
|
||
// post can cost is MinFee + size × PostByteFee. A 0-byte post is
|
||
// rejected (Size must be > 0), so in practice a ~50-byte text post
|
||
// costs ~1050 µT (~$0.001 depending on token price).
|
||
BasePostFee uint64 = 1_000 // 0.001 T flat per post — aligned with MinFee floor
|
||
PostByteFee uint64 = 1 // 1 µT per byte of stored content
|
||
|
||
// MaxPostSize caps a single post's on-wire size (text + attachment, post
|
||
// compression). Hard limit — node refuses larger envelopes to protect
|
||
// storage and bandwidth.
|
||
MaxPostSize uint64 = 256 * 1024 // 256 KiB
|
||
|
||
// LikeFee / FollowFee / UnlikeFee / UnfollowFee / DeletePostFee all use
|
||
// MinFee (1000 µT) — standard tx fee paid to the validator. No extra
|
||
// cost; these events carry no body.
|
||
)
|
||
|
||
// 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
|
||
}
|
||
|
||
// ── Feed payloads (v2.0.0) ─────────────────────────────────────────────────
|
||
|
||
// CreatePostPayload is embedded in EventCreatePost transactions. The body
|
||
// itself is NOT stored on-chain — it lives in the relay feed-mailbox keyed
|
||
// by PostID. On-chain we only keep author, size, hash, timestamp and any
|
||
// reply/quote reference for ordering and proof of authorship.
|
||
//
|
||
// PostID is computed client-side as hex(sha256(author || content_hash || ts)[:16])
|
||
// — same scheme as envelope IDs. Clients include it so the relay can store
|
||
// the body under a stable key before the chain commit lands.
|
||
//
|
||
// HostingRelay is the node pubkey (Ed25519 hex) that accepted the POST
|
||
// /feed/publish call and holds the body. Readers resolve it via the chain
|
||
// and fetch the body directly from that relay (or via gossipsub replicas).
|
||
// The fee is credited to this pub.
|
||
//
|
||
// QuoteOf / ReplyTo are mutually exclusive; set at most one. ReplyTo makes
|
||
// the post a reply in a thread; QuoteOf creates a link/reference block.
|
||
type CreatePostPayload struct {
|
||
PostID string `json:"post_id"`
|
||
ContentHash []byte `json:"content_hash"` // sha256 of body-bytes, 32 B
|
||
Size uint64 `json:"size"` // bytes on disk (compressed)
|
||
HostingRelay string `json:"hosting_relay"` // hex Ed25519 of storing node
|
||
ReplyTo string `json:"reply_to,omitempty"` // parent post ID
|
||
QuoteOf string `json:"quote_of,omitempty"` // referenced post ID
|
||
}
|
||
|
||
// DeletePostPayload — author soft-deletes their own post. Stored marker
|
||
// lets clients hide the post; relay can GC the body on the next sweep.
|
||
type DeletePostPayload struct {
|
||
PostID string `json:"post_id"`
|
||
}
|
||
|
||
// FollowPayload / UnfollowPayload — follow graph. tx.From = follower,
|
||
// tx.To = target. No body.
|
||
type FollowPayload struct{}
|
||
type UnfollowPayload struct{}
|
||
|
||
// LikePostPayload / UnlikePostPayload — per-post like indicator. tx.From
|
||
// = liker. The counter is derived on read.
|
||
type LikePostPayload struct {
|
||
PostID string `json:"post_id"`
|
||
}
|
||
type UnlikePostPayload struct {
|
||
PostID string `json:"post_id"`
|
||
}
|
||
|
||
// PostRecord is what we store on-chain under post:<postID>. Consumers of
|
||
// PostsByAuthor / query endpoints decode this.
|
||
type PostRecord struct {
|
||
PostID string `json:"post_id"`
|
||
Author string `json:"author"` // hex Ed25519
|
||
ContentHash []byte `json:"content_hash"`
|
||
Size uint64 `json:"size"`
|
||
HostingRelay string `json:"hosting_relay"`
|
||
ReplyTo string `json:"reply_to,omitempty"`
|
||
QuoteOf string `json:"quote_of,omitempty"`
|
||
CreatedAt int64 `json:"created_at"` // unix seconds (tx timestamp)
|
||
Deleted bool `json:"deleted,omitempty"`
|
||
FeeUT uint64 `json:"fee_ut"` // total fee paid
|
||
}
|
||
|
||
// 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"`
|
||
}
|
||
|
||
// 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"`
|
||
}
|