feat(chain): remove channels, add social feed (Phase A of v2.0.0)

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>
This commit is contained in:
vsecoder
2026-04-18 18:36:00 +03:00
parent f2cb5586ca
commit 88848efa63
6 changed files with 695 additions and 168 deletions

View File

@@ -11,8 +11,6 @@ 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"
@@ -37,6 +35,17 @@ const (
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).
@@ -64,6 +73,31 @@ const (
// 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.
@@ -90,11 +124,66 @@ type RegisterKeyPayload struct {
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"`
// ── 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.
@@ -241,24 +330,6 @@ 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.
//