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>
1042 lines
33 KiB
Go
1042 lines
33 KiB
Go
package blockchain_test
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"go-blockchain/blockchain"
|
|
"go-blockchain/identity"
|
|
)
|
|
|
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
// newChain opens a fresh BadgerDB-backed chain in a temp directory and
|
|
// registers a cleanup that closes the DB then removes the directory.
|
|
// We avoid t.TempDir() because on Windows, BadgerDB's mmap'd value-log files
|
|
// may still be held open for a brief moment after Close() returns, causing
|
|
// the automatic TempDir cleanup to fail with "directory not empty".
|
|
// Using os.MkdirTemp + a retry loop works around this race.
|
|
func newChain(t *testing.T) *blockchain.Chain {
|
|
t.Helper()
|
|
dir, err := os.MkdirTemp("", "dchain-test-*")
|
|
if err != nil {
|
|
t.Fatalf("MkdirTemp: %v", err)
|
|
}
|
|
c, err := blockchain.NewChain(dir)
|
|
if err != nil {
|
|
_ = os.RemoveAll(dir)
|
|
t.Fatalf("NewChain: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = c.Close()
|
|
// Retry removal to handle Windows mmap handle release delay.
|
|
for i := 0; i < 20; i++ {
|
|
if err := os.RemoveAll(dir); err == nil {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
})
|
|
return c
|
|
}
|
|
|
|
// newIdentity generates a fresh Ed25519 + X25519 keypair for test use.
|
|
func newIdentity(t *testing.T) *identity.Identity {
|
|
t.Helper()
|
|
id, err := identity.Generate()
|
|
if err != nil {
|
|
t.Fatalf("identity.Generate: %v", err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// addGenesis creates and commits the genesis block signed by validator.
|
|
func addGenesis(t *testing.T, c *blockchain.Chain, validator *identity.Identity) *blockchain.Block {
|
|
t.Helper()
|
|
b := blockchain.GenesisBlock(validator.PubKeyHex(), validator.PrivKey)
|
|
if err := c.AddBlock(b); err != nil {
|
|
t.Fatalf("AddBlock(genesis): %v", err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// txID produces a short deterministic transaction ID.
|
|
func txID(from string, typ blockchain.EventType) string {
|
|
h := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%d", from, typ, time.Now().UnixNano())))
|
|
return hex.EncodeToString(h[:16])
|
|
}
|
|
|
|
// makeTx builds a minimal transaction with all required fields set.
|
|
// Signature is intentionally left nil — chain.applyTx does not re-verify
|
|
// Ed25519 tx signatures (that is the consensus engine's job).
|
|
func makeTx(typ blockchain.EventType, from, to string, amount, fee uint64, payload []byte) *blockchain.Transaction {
|
|
return &blockchain.Transaction{
|
|
ID: txID(from, typ),
|
|
Type: typ,
|
|
From: from,
|
|
To: to,
|
|
Amount: amount,
|
|
Fee: fee,
|
|
Payload: payload,
|
|
Timestamp: time.Now().UTC(),
|
|
}
|
|
}
|
|
|
|
// mustJSON marshals v and panics on error (test helper only).
|
|
func mustJSON(v any) []byte {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// buildBlock wraps txs in a block that follows prev, computes hash, and signs
|
|
// it with validatorPriv. TotalFees is computed from the tx slice.
|
|
func buildBlock(t *testing.T, prev *blockchain.Block, validator *identity.Identity, txs []*blockchain.Transaction) *blockchain.Block {
|
|
t.Helper()
|
|
var totalFees uint64
|
|
for _, tx := range txs {
|
|
totalFees += tx.Fee
|
|
}
|
|
b := &blockchain.Block{
|
|
Index: prev.Index + 1,
|
|
Timestamp: time.Now().UTC(),
|
|
Transactions: txs,
|
|
PrevHash: prev.Hash,
|
|
Validator: validator.PubKeyHex(),
|
|
TotalFees: totalFees,
|
|
}
|
|
b.ComputeHash()
|
|
b.Sign(validator.PrivKey)
|
|
return b
|
|
}
|
|
|
|
// mustAddBlock calls c.AddBlock and fails the test on error.
|
|
func mustAddBlock(t *testing.T, c *blockchain.Chain, b *blockchain.Block) {
|
|
t.Helper()
|
|
if err := c.AddBlock(b); err != nil {
|
|
t.Fatalf("AddBlock (index %d): %v", b.Index, err)
|
|
}
|
|
}
|
|
|
|
// mustBalance reads the balance and fails on error.
|
|
func mustBalance(t *testing.T, c *blockchain.Chain, pubHex string) uint64 {
|
|
t.Helper()
|
|
bal, err := c.Balance(pubHex)
|
|
if err != nil {
|
|
t.Fatalf("Balance(%s): %v", pubHex[:8], err)
|
|
}
|
|
return bal
|
|
}
|
|
|
|
// ─── tests ───────────────────────────────────────────────────────────────────
|
|
|
|
// 1. Genesis block credits GenesisAllocation to the validator.
|
|
func TestGenesisCreatesBalance(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
|
|
addGenesis(t, c, val)
|
|
|
|
bal := mustBalance(t, c, val.PubKeyHex())
|
|
if bal != blockchain.GenesisAllocation {
|
|
t.Errorf("expected GenesisAllocation=%d, got %d", blockchain.GenesisAllocation, bal)
|
|
}
|
|
}
|
|
|
|
// 2. Transfer moves tokens between two identities and leaves correct balances.
|
|
func TestTransfer(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Fund alice via a transfer from validator.
|
|
const sendAmount = 100 * blockchain.Token
|
|
const fee = blockchain.MinFee
|
|
|
|
tx := makeTx(
|
|
blockchain.EventTransfer,
|
|
val.PubKeyHex(),
|
|
alice.PubKeyHex(),
|
|
sendAmount, fee,
|
|
mustJSON(blockchain.TransferPayload{}),
|
|
)
|
|
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{tx})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
valBal := mustBalance(t, c, val.PubKeyHex())
|
|
aliceBal := mustBalance(t, c, alice.PubKeyHex())
|
|
|
|
// Validator: genesis - sendAmount - fee + fee (validator earns TotalFees back)
|
|
expectedVal := blockchain.GenesisAllocation - sendAmount - fee + fee
|
|
if valBal != expectedVal {
|
|
t.Errorf("validator balance: got %d, want %d", valBal, expectedVal)
|
|
}
|
|
if aliceBal != sendAmount {
|
|
t.Errorf("alice balance: got %d, want %d", aliceBal, sendAmount)
|
|
}
|
|
}
|
|
|
|
// 3. Transfer that exceeds sender's balance must fail AddBlock.
|
|
func TestTransferInsufficientFunds(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// alice has 0 balance — try to spend 1 token
|
|
tx := makeTx(
|
|
blockchain.EventTransfer,
|
|
alice.PubKeyHex(),
|
|
val.PubKeyHex(),
|
|
1*blockchain.Token, blockchain.MinFee,
|
|
mustJSON(blockchain.TransferPayload{}),
|
|
)
|
|
b := buildBlock(t, genesis, val, []*blockchain.Transaction{tx})
|
|
|
|
// AddBlock must succeed — the bad tx is skipped rather than rejecting the block.
|
|
if err := c.AddBlock(b); err != nil {
|
|
t.Fatalf("AddBlock returned unexpected error: %v", err)
|
|
}
|
|
// Alice's balance must still be 0 — the skipped tx had no effect.
|
|
bal, err := c.Balance(alice.PubKeyHex())
|
|
if err != nil {
|
|
t.Fatalf("Balance: %v", err)
|
|
}
|
|
if bal != 0 {
|
|
t.Errorf("expected alice balance 0, got %d", bal)
|
|
}
|
|
}
|
|
|
|
// 4. EventRegisterKey stores X25519 key in IdentityInfo.
|
|
func TestRegisterKeyStoresIdentity(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
payload := blockchain.RegisterKeyPayload{
|
|
PubKey: alice.PubKeyHex(),
|
|
Nickname: "alice",
|
|
PowNonce: 0,
|
|
PowTarget: "0",
|
|
X25519PubKey: alice.X25519PubHex(),
|
|
}
|
|
tx := makeTx(
|
|
blockchain.EventRegisterKey,
|
|
alice.PubKeyHex(),
|
|
"",
|
|
0, blockchain.RegistrationFee,
|
|
mustJSON(payload),
|
|
)
|
|
|
|
// Fund alice with enough to cover RegistrationFee before she registers.
|
|
fundTx := makeTx(
|
|
blockchain.EventTransfer,
|
|
val.PubKeyHex(),
|
|
alice.PubKeyHex(),
|
|
blockchain.RegistrationFee, blockchain.MinFee,
|
|
mustJSON(blockchain.TransferPayload{}),
|
|
)
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundTx})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{tx})
|
|
mustAddBlock(t, c, b2)
|
|
|
|
info, err := c.IdentityInfo(alice.PubKeyHex())
|
|
if err != nil {
|
|
t.Fatalf("IdentityInfo: %v", err)
|
|
}
|
|
if !info.Registered {
|
|
t.Error("expected Registered=true after REGISTER_KEY tx")
|
|
}
|
|
if info.Nickname != "alice" {
|
|
t.Errorf("nickname: got %q, want %q", info.Nickname, "alice")
|
|
}
|
|
if info.X25519Pub != alice.X25519PubHex() {
|
|
t.Errorf("X25519Pub: got %q, want %q", info.X25519Pub, alice.X25519PubHex())
|
|
}
|
|
}
|
|
|
|
// 5. ContactRequest flow: pending → accepted → blocked.
|
|
func TestContactRequestFlow(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t) // requester
|
|
bob := newIdentity(t) // target
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Fund alice and bob for fees.
|
|
const contactAmt = blockchain.MinContactFee
|
|
fundAlice := makeTx(blockchain.EventTransfer, val.PubKeyHex(), alice.PubKeyHex(),
|
|
contactAmt+2*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
fundBob := makeTx(blockchain.EventTransfer, val.PubKeyHex(), bob.PubKeyHex(),
|
|
2*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundAlice, fundBob})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
// Alice sends contact request to Bob.
|
|
reqTx := makeTx(
|
|
blockchain.EventContactRequest,
|
|
alice.PubKeyHex(),
|
|
bob.PubKeyHex(),
|
|
contactAmt, blockchain.MinFee,
|
|
mustJSON(blockchain.ContactRequestPayload{Intro: "Hey Bob!"}),
|
|
)
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{reqTx})
|
|
mustAddBlock(t, c, b2)
|
|
|
|
contacts, err := c.ContactRequests(bob.PubKeyHex())
|
|
if err != nil {
|
|
t.Fatalf("ContactRequests: %v", err)
|
|
}
|
|
if len(contacts) != 1 {
|
|
t.Fatalf("expected 1 contact record, got %d", len(contacts))
|
|
}
|
|
if contacts[0].Status != blockchain.ContactPending {
|
|
t.Errorf("status: got %q, want %q", contacts[0].Status, blockchain.ContactPending)
|
|
}
|
|
|
|
// Bob accepts.
|
|
acceptTx := makeTx(
|
|
blockchain.EventAcceptContact,
|
|
bob.PubKeyHex(),
|
|
alice.PubKeyHex(),
|
|
0, blockchain.MinFee,
|
|
mustJSON(blockchain.AcceptContactPayload{}),
|
|
)
|
|
b3 := buildBlock(t, b2, val, []*blockchain.Transaction{acceptTx})
|
|
mustAddBlock(t, c, b3)
|
|
|
|
contacts, err = c.ContactRequests(bob.PubKeyHex())
|
|
if err != nil {
|
|
t.Fatalf("ContactRequests after accept: %v", err)
|
|
}
|
|
if len(contacts) != 1 || contacts[0].Status != blockchain.ContactAccepted {
|
|
t.Errorf("expected accepted, got %v", contacts)
|
|
}
|
|
|
|
// Bob then blocks Alice (status transitions from accepted → blocked).
|
|
blockTx := makeTx(
|
|
blockchain.EventBlockContact,
|
|
bob.PubKeyHex(),
|
|
alice.PubKeyHex(),
|
|
0, blockchain.MinFee,
|
|
mustJSON(blockchain.BlockContactPayload{}),
|
|
)
|
|
b4 := buildBlock(t, b3, val, []*blockchain.Transaction{blockTx})
|
|
mustAddBlock(t, c, b4)
|
|
|
|
contacts, err = c.ContactRequests(bob.PubKeyHex())
|
|
if err != nil {
|
|
t.Fatalf("ContactRequests after block: %v", err)
|
|
}
|
|
if len(contacts) != 1 || contacts[0].Status != blockchain.ContactBlocked {
|
|
t.Errorf("expected blocked, got %v", contacts)
|
|
}
|
|
}
|
|
|
|
// 6. ContactRequest with amount below MinContactFee must fail.
|
|
func TestContactRequestInsufficientFee(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t)
|
|
bob := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Fund alice.
|
|
fundAlice := makeTx(blockchain.EventTransfer, val.PubKeyHex(), alice.PubKeyHex(),
|
|
blockchain.MinContactFee+blockchain.MinFee, blockchain.MinFee,
|
|
mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundAlice})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
// Amount is one µT below MinContactFee.
|
|
reqTx := makeTx(
|
|
blockchain.EventContactRequest,
|
|
alice.PubKeyHex(),
|
|
bob.PubKeyHex(),
|
|
blockchain.MinContactFee-1, blockchain.MinFee,
|
|
mustJSON(blockchain.ContactRequestPayload{}),
|
|
)
|
|
b := buildBlock(t, b1, val, []*blockchain.Transaction{reqTx})
|
|
// AddBlock must succeed — the bad tx is skipped rather than rejecting the block.
|
|
if err := c.AddBlock(b); err != nil {
|
|
t.Fatalf("AddBlock returned unexpected error: %v", err)
|
|
}
|
|
// No pending contact record must exist for bob←alice.
|
|
contacts, err := c.ContactRequests(bob.PubKeyHex())
|
|
if err != nil {
|
|
t.Fatalf("ContactRequests: %v", err)
|
|
}
|
|
if len(contacts) != 0 {
|
|
t.Errorf("expected 0 pending contacts, got %d (tx should have been skipped)", len(contacts))
|
|
}
|
|
}
|
|
|
|
// 7. InitValidators seeds keys; ValidatorSet returns them all.
|
|
func TestValidatorSetInit(t *testing.T) {
|
|
c := newChain(t)
|
|
ids := []*identity.Identity{newIdentity(t), newIdentity(t), newIdentity(t)}
|
|
keys := make([]string, len(ids))
|
|
for i, id := range ids {
|
|
keys[i] = id.PubKeyHex()
|
|
}
|
|
|
|
if err := c.InitValidators(keys); err != nil {
|
|
t.Fatalf("InitValidators: %v", err)
|
|
}
|
|
|
|
set, err := c.ValidatorSet()
|
|
if err != nil {
|
|
t.Fatalf("ValidatorSet: %v", err)
|
|
}
|
|
if len(set) != len(keys) {
|
|
t.Fatalf("expected %d validators, got %d", len(keys), len(set))
|
|
}
|
|
got := make(map[string]bool, len(set))
|
|
for _, k := range set {
|
|
got[k] = true
|
|
}
|
|
for _, k := range keys {
|
|
if !got[k] {
|
|
t.Errorf("key %s missing from validator set", k[:8])
|
|
}
|
|
}
|
|
}
|
|
|
|
// 8. EventAddValidator adds a new validator via a real block.
|
|
//
|
|
// Updated for P2.1 (stake-gated admission): the candidate must first have
|
|
// at least MinValidatorStake (1 T = 1_000_000 µT) locked via a STAKE tx
|
|
// and be credited enough balance to do so. Multi-sig approval is trivially
|
|
// met here because the initial set has only one validator — ⌈2/3⌉ of 1
|
|
// is 1, which the tx sender provides implicitly.
|
|
func TestAddValidatorTx(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t) // initial validator
|
|
newVal := newIdentity(t) // to be added
|
|
|
|
// Seed the initial validator.
|
|
if err := c.InitValidators([]string{val.PubKeyHex()}); err != nil {
|
|
t.Fatalf("InitValidators: %v", err)
|
|
}
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Fund the candidate enough to stake.
|
|
fundTx := makeTx(
|
|
blockchain.EventTransfer,
|
|
val.PubKeyHex(),
|
|
newVal.PubKeyHex(),
|
|
2*blockchain.MinValidatorStake, blockchain.MinFee,
|
|
mustJSON(blockchain.TransferPayload{}),
|
|
)
|
|
// Candidate stakes the minimum.
|
|
stakeTx := makeTx(
|
|
blockchain.EventStake,
|
|
newVal.PubKeyHex(),
|
|
newVal.PubKeyHex(),
|
|
blockchain.MinValidatorStake, blockchain.MinFee,
|
|
nil,
|
|
)
|
|
preBlock := buildBlock(t, genesis, val, []*blockchain.Transaction{fundTx, stakeTx})
|
|
mustAddBlock(t, c, preBlock)
|
|
|
|
tx := makeTx(
|
|
blockchain.EventAddValidator,
|
|
val.PubKeyHex(),
|
|
newVal.PubKeyHex(),
|
|
0, blockchain.MinFee,
|
|
mustJSON(blockchain.AddValidatorPayload{Reason: "test"}),
|
|
)
|
|
b1 := buildBlock(t, preBlock, val, []*blockchain.Transaction{tx})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
set, err := c.ValidatorSet()
|
|
if err != nil {
|
|
t.Fatalf("ValidatorSet: %v", err)
|
|
}
|
|
found := false
|
|
for _, k := range set {
|
|
if k == newVal.PubKeyHex() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("new validator %s not found in set after ADD_VALIDATOR tx", newVal.PubKeyHex()[:8])
|
|
}
|
|
}
|
|
|
|
// 9. EventRemoveValidator removes a key from the set.
|
|
//
|
|
// Updated for P2.2 (multi-sig forced removal): the sender and the
|
|
// cosigners must together reach ⌈2/3⌉ of the current set. Here we have
|
|
// 3 validators, so 2 approvals are needed. `val` sends, `coSigner` adds
|
|
// a signature for RemoveDigest(removeMe.Pub).
|
|
func TestRemoveValidatorTx(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
coSigner := newIdentity(t)
|
|
removeMe := newIdentity(t)
|
|
|
|
// All three start as validators (ceil(2/3 * 3) = 2 approvals needed).
|
|
if err := c.InitValidators([]string{val.PubKeyHex(), coSigner.PubKeyHex(), removeMe.PubKeyHex()}); err != nil {
|
|
t.Fatalf("InitValidators: %v", err)
|
|
}
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// coSigner produces an off-chain approval for removing removeMe.
|
|
sig := coSigner.Sign(blockchain.RemoveDigest(removeMe.PubKeyHex()))
|
|
|
|
tx := makeTx(
|
|
blockchain.EventRemoveValidator,
|
|
val.PubKeyHex(),
|
|
removeMe.PubKeyHex(),
|
|
0, blockchain.MinFee,
|
|
mustJSON(blockchain.RemoveValidatorPayload{
|
|
Reason: "test",
|
|
CoSignatures: []blockchain.ValidatorCoSig{
|
|
{PubKey: coSigner.PubKeyHex(), Signature: sig},
|
|
},
|
|
}),
|
|
)
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{tx})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
set, err := c.ValidatorSet()
|
|
if err != nil {
|
|
t.Fatalf("ValidatorSet: %v", err)
|
|
}
|
|
for _, k := range set {
|
|
if k == removeMe.PubKeyHex() {
|
|
t.Errorf("removed validator %s still in set", removeMe.PubKeyHex()[:8])
|
|
}
|
|
}
|
|
}
|
|
|
|
// 10. ADD_VALIDATOR tx from a non-validator must fail.
|
|
func TestAddValidatorNotAValidator(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
nonVal := newIdentity(t)
|
|
target := newIdentity(t)
|
|
|
|
if err := c.InitValidators([]string{val.PubKeyHex()}); err != nil {
|
|
t.Fatalf("InitValidators: %v", err)
|
|
}
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Fund nonVal so the debit doesn't fail first (it should fail on validator check).
|
|
fundTx := makeTx(blockchain.EventTransfer, val.PubKeyHex(), nonVal.PubKeyHex(),
|
|
10*blockchain.Token, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundTx})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
badTx := makeTx(
|
|
blockchain.EventAddValidator,
|
|
nonVal.PubKeyHex(), // not a validator
|
|
target.PubKeyHex(),
|
|
0, blockchain.MinFee,
|
|
mustJSON(blockchain.AddValidatorPayload{}),
|
|
)
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{badTx})
|
|
// AddBlock must succeed — the bad tx is skipped rather than rejecting the block.
|
|
if err := c.AddBlock(b2); err != nil {
|
|
t.Fatalf("AddBlock returned unexpected error: %v", err)
|
|
}
|
|
// target must NOT have been added as a validator (tx was skipped).
|
|
vset, err := c.ValidatorSet()
|
|
if err != nil {
|
|
t.Fatalf("ValidatorSet: %v", err)
|
|
}
|
|
for _, v := range vset {
|
|
if v == target.PubKeyHex() {
|
|
t.Error("target was added as validator despite tx being from a non-validator (should have been skipped)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 11. RelayProof with valid FeeSig transfers the relay fee from sender to relay.
|
|
func TestRelayProofClaimsFee(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
sender := newIdentity(t)
|
|
relay := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
const relayFeeUT = 5_000 * blockchain.MicroToken
|
|
|
|
// Fund sender with enough to cover relay fee and tx fee.
|
|
fundTx := makeTx(blockchain.EventTransfer, val.PubKeyHex(), sender.PubKeyHex(),
|
|
relayFeeUT+blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundTx})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
senderBalBefore := mustBalance(t, c, sender.PubKeyHex())
|
|
relayBalBefore := mustBalance(t, c, relay.PubKeyHex())
|
|
|
|
envelopeID := "env-abc123"
|
|
authBytes := blockchain.FeeAuthBytes(envelopeID, relayFeeUT)
|
|
feeSig := sender.Sign(authBytes)
|
|
|
|
envelopeHash := sha256.Sum256([]byte("fake-ciphertext"))
|
|
proofPayload := blockchain.RelayProofPayload{
|
|
EnvelopeID: envelopeID,
|
|
EnvelopeHash: envelopeHash[:],
|
|
SenderPubKey: sender.PubKeyHex(),
|
|
FeeUT: relayFeeUT,
|
|
FeeSig: feeSig,
|
|
RelayPubKey: relay.PubKeyHex(),
|
|
DeliveredAt: time.Now().Unix(),
|
|
}
|
|
tx := makeTx(
|
|
blockchain.EventRelayProof,
|
|
relay.PubKeyHex(),
|
|
"",
|
|
0, blockchain.MinFee,
|
|
mustJSON(proofPayload),
|
|
)
|
|
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{tx})
|
|
mustAddBlock(t, c, b2)
|
|
|
|
senderBalAfter := mustBalance(t, c, sender.PubKeyHex())
|
|
relayBalAfter := mustBalance(t, c, relay.PubKeyHex())
|
|
|
|
if senderBalAfter != senderBalBefore-relayFeeUT {
|
|
t.Errorf("sender balance: got %d, want %d (before %d - fee %d)",
|
|
senderBalAfter, senderBalBefore-relayFeeUT, senderBalBefore, relayFeeUT)
|
|
}
|
|
if relayBalAfter != relayBalBefore+relayFeeUT {
|
|
t.Errorf("relay balance: got %d, want %d (before %d + fee %d)",
|
|
relayBalAfter, relayBalBefore+relayFeeUT, relayBalBefore, relayFeeUT)
|
|
}
|
|
}
|
|
|
|
// 12. RelayProof with wrong FeeSig must fail AddBlock.
|
|
func TestRelayProofBadSig(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
sender := newIdentity(t)
|
|
relay := newIdentity(t)
|
|
imposter := newIdentity(t) // signs instead of sender
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
const relayFeeUT = 5_000 * blockchain.MicroToken
|
|
|
|
// Fund sender.
|
|
fundTx := makeTx(blockchain.EventTransfer, val.PubKeyHex(), sender.PubKeyHex(),
|
|
relayFeeUT+blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundTx})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
senderBalBefore := mustBalance(t, c, sender.PubKeyHex())
|
|
|
|
envelopeID := "env-xyz"
|
|
authBytes := blockchain.FeeAuthBytes(envelopeID, relayFeeUT)
|
|
// Imposter signs, not the actual sender.
|
|
badFeeSig := imposter.Sign(authBytes)
|
|
|
|
envelopeHash := sha256.Sum256([]byte("ciphertext"))
|
|
proofPayload := blockchain.RelayProofPayload{
|
|
EnvelopeID: envelopeID,
|
|
EnvelopeHash: envelopeHash[:],
|
|
SenderPubKey: sender.PubKeyHex(), // claims sender, but sig is from imposter
|
|
FeeUT: relayFeeUT,
|
|
FeeSig: badFeeSig,
|
|
RelayPubKey: relay.PubKeyHex(),
|
|
DeliveredAt: time.Now().Unix(),
|
|
}
|
|
tx := makeTx(
|
|
blockchain.EventRelayProof,
|
|
relay.PubKeyHex(),
|
|
"",
|
|
0, blockchain.MinFee,
|
|
mustJSON(proofPayload),
|
|
)
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{tx})
|
|
// AddBlock must succeed — the bad tx is skipped rather than rejecting the block.
|
|
if err := c.AddBlock(b2); err != nil {
|
|
t.Fatalf("AddBlock returned unexpected error: %v", err)
|
|
}
|
|
// Sender's balance must be unchanged — the skipped tx had no effect.
|
|
senderBalAfter, err := c.Balance(sender.PubKeyHex())
|
|
if err != nil {
|
|
t.Fatalf("Balance: %v", err)
|
|
}
|
|
if senderBalAfter != senderBalBefore {
|
|
t.Errorf("sender balance changed despite bad-sig tx: before=%d after=%d",
|
|
senderBalBefore, senderBalAfter)
|
|
}
|
|
}
|
|
|
|
// 13. Adding the same block index twice must fail.
|
|
func TestDuplicateBlockRejected(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Build block 1.
|
|
b1 := buildBlock(t, genesis, val, nil)
|
|
mustAddBlock(t, c, b1)
|
|
|
|
// Build an independent block also claiming index 1 (different hash).
|
|
b1dup := &blockchain.Block{
|
|
Index: 1,
|
|
Timestamp: time.Now().Add(time.Millisecond).UTC(),
|
|
Transactions: []*blockchain.Transaction{},
|
|
PrevHash: genesis.Hash,
|
|
Validator: val.PubKeyHex(),
|
|
TotalFees: 0,
|
|
}
|
|
b1dup.ComputeHash()
|
|
b1dup.Sign(val.PrivKey)
|
|
|
|
// The chain tip is already at index 1; the new block has index 1 but a
|
|
// different prevHash (its own prev is genesis too but tip.Hash ≠ genesis.Hash).
|
|
if err := c.AddBlock(b1dup); err == nil {
|
|
t.Fatal("expected AddBlock to fail for duplicate index, but it succeeded")
|
|
}
|
|
}
|
|
|
|
// 14. Block with wrong prevHash must fail.
|
|
func TestChainLinkageRejected(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Create a block with a garbage prevHash.
|
|
garbagePrev := make([]byte, 32)
|
|
if _, err := rand.Read(garbagePrev); err != nil {
|
|
t.Fatalf("rand.Read: %v", err)
|
|
}
|
|
badBlock := &blockchain.Block{
|
|
Index: 1,
|
|
Timestamp: time.Now().UTC(),
|
|
Transactions: []*blockchain.Transaction{},
|
|
PrevHash: garbagePrev,
|
|
Validator: val.PubKeyHex(),
|
|
TotalFees: 0,
|
|
}
|
|
badBlock.ComputeHash()
|
|
badBlock.Sign(val.PrivKey)
|
|
|
|
if err := c.AddBlock(badBlock); err == nil {
|
|
t.Fatal("expected AddBlock to fail for wrong prevHash, but it succeeded")
|
|
}
|
|
|
|
// Tip must still be genesis.
|
|
tip := c.Tip()
|
|
if tip.Index != genesis.Index {
|
|
t.Errorf("tip index after rejection: got %d, want %d", tip.Index, genesis.Index)
|
|
}
|
|
}
|
|
|
|
// 15. Tip advances with each successfully committed block.
|
|
func TestTipUpdates(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
|
|
if tip := c.Tip(); tip != nil {
|
|
t.Fatalf("tip on empty chain: expected nil, got index %d", tip.Index)
|
|
}
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
if tip := c.Tip(); tip == nil || tip.Index != 0 {
|
|
t.Fatalf("tip after genesis: expected index 0, got %v", tip)
|
|
}
|
|
|
|
prev := genesis
|
|
for i := uint64(1); i <= 3; i++ {
|
|
b := buildBlock(t, prev, val, nil)
|
|
mustAddBlock(t, c, b)
|
|
|
|
tip := c.Tip()
|
|
if tip == nil {
|
|
t.Fatalf("tip is nil after block %d", i)
|
|
}
|
|
if tip.Index != i {
|
|
t.Errorf("tip.Index after block %d: got %d, want %d", i, tip.Index, i)
|
|
}
|
|
prev = b
|
|
}
|
|
}
|
|
|
|
// ─── compile-time guard ──────────────────────────────────────────────────────
|
|
|
|
// Ensure the identity package is used directly so the import is not trimmed.
|
|
var _ = identity.Generate
|
|
|
|
// Ensure ed25519 and hex are used directly (they may be used via helpers).
|
|
var _ = ed25519.PublicKey(nil)
|
|
var _ = hex.EncodeToString
|
|
|
|
// ── Feed (v2.0.0) ──────────────────────────────────────────────────────────
|
|
|
|
// TestFeedCreatePost: post commits, indexes, credits the hosting relay.
|
|
func TestFeedCreatePost(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t) // post author
|
|
host := newIdentity(t) // hosting relay pubkey
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
// Fund alice + host.
|
|
const postSize = uint64(200)
|
|
expectedFee := blockchain.BasePostFee + postSize*blockchain.PostByteFee
|
|
fundAlice := makeTx(blockchain.EventTransfer, val.PubKeyHex(), alice.PubKeyHex(),
|
|
expectedFee+5*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
time.Sleep(2 * time.Millisecond) // ensure distinct txID (nanosec clock)
|
|
fundHost := makeTx(blockchain.EventTransfer, val.PubKeyHex(), host.PubKeyHex(),
|
|
blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundAlice, fundHost})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
hostBalBefore, _ := c.Balance(host.PubKeyHex())
|
|
|
|
h := sha256.Sum256([]byte("hello world post body"))
|
|
postPayload := blockchain.CreatePostPayload{
|
|
PostID: "post1",
|
|
ContentHash: h[:],
|
|
Size: postSize,
|
|
HostingRelay: host.PubKeyHex(),
|
|
}
|
|
postTx := makeTx(
|
|
blockchain.EventCreatePost,
|
|
alice.PubKeyHex(), "",
|
|
0, expectedFee, // Fee = base + size*byte_fee; amount = 0
|
|
mustJSON(postPayload),
|
|
)
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{postTx})
|
|
mustAddBlock(t, c, b2)
|
|
|
|
rec, err := c.Post("post1")
|
|
if err != nil || rec == nil {
|
|
t.Fatalf("Post(\"post1\") = %v, %v; want record", rec, err)
|
|
}
|
|
if rec.Author != alice.PubKeyHex() {
|
|
t.Errorf("author: got %q want %q", rec.Author, alice.PubKeyHex())
|
|
}
|
|
if rec.Size != postSize {
|
|
t.Errorf("size: got %d want %d", rec.Size, postSize)
|
|
}
|
|
|
|
// Host should have been credited the full fee.
|
|
hostBalAfter, _ := c.Balance(host.PubKeyHex())
|
|
if hostBalAfter != hostBalBefore+expectedFee {
|
|
t.Errorf("host balance: got %d, want %d (delta %d)",
|
|
hostBalAfter, hostBalBefore, expectedFee)
|
|
}
|
|
|
|
// PostsByAuthor should list it.
|
|
posts, err := c.PostsByAuthor(alice.PubKeyHex(), 10)
|
|
if err != nil {
|
|
t.Fatalf("PostsByAuthor: %v", err)
|
|
}
|
|
if len(posts) != 1 || posts[0].PostID != "post1" {
|
|
t.Errorf("PostsByAuthor: got %v, want [post1]", posts)
|
|
}
|
|
}
|
|
|
|
// TestFeedInsufficientFee: size-based fee is enforced.
|
|
func TestFeedInsufficientFee(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t)
|
|
host := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
fundAlice := makeTx(blockchain.EventTransfer, val.PubKeyHex(), alice.PubKeyHex(),
|
|
10*blockchain.Token, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundAlice})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
const postSize = uint64(1000)
|
|
h := sha256.Sum256([]byte("body"))
|
|
postPayload := blockchain.CreatePostPayload{
|
|
PostID: "underpaid",
|
|
ContentHash: h[:],
|
|
Size: postSize,
|
|
HostingRelay: host.PubKeyHex(),
|
|
}
|
|
// Fee too low — base alone without the size component.
|
|
// (Must still be ≥ MinFee so the chain-level block validation passes;
|
|
// the per-event CREATE_POST check is what should reject it.)
|
|
postTx := makeTx(blockchain.EventCreatePost, alice.PubKeyHex(), "",
|
|
0, blockchain.MinFee, mustJSON(postPayload))
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{postTx})
|
|
mustAddBlock(t, c, b2) // block commits, the tx is skipped (logged)
|
|
|
|
if rec, _ := c.Post("underpaid"); rec != nil {
|
|
t.Fatalf("post was stored despite insufficient fee: %+v", rec)
|
|
}
|
|
}
|
|
|
|
// TestFeedFollowUnfollow: follow graph round-trips via indices.
|
|
func TestFeedFollowUnfollow(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t)
|
|
bob := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
fundAlice := makeTx(blockchain.EventTransfer, val.PubKeyHex(), alice.PubKeyHex(),
|
|
5*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundAlice})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
followTx := makeTx(blockchain.EventFollow, alice.PubKeyHex(), bob.PubKeyHex(),
|
|
0, blockchain.MinFee, mustJSON(blockchain.FollowPayload{}))
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{followTx})
|
|
mustAddBlock(t, c, b2)
|
|
|
|
following, _ := c.Following(alice.PubKeyHex())
|
|
if len(following) != 1 || following[0] != bob.PubKeyHex() {
|
|
t.Errorf("Following: got %v, want [%s]", following, bob.PubKeyHex())
|
|
}
|
|
followers, _ := c.Followers(bob.PubKeyHex())
|
|
if len(followers) != 1 || followers[0] != alice.PubKeyHex() {
|
|
t.Errorf("Followers: got %v, want [%s]", followers, alice.PubKeyHex())
|
|
}
|
|
|
|
// Unfollow.
|
|
unfollowTx := makeTx(blockchain.EventUnfollow, alice.PubKeyHex(), bob.PubKeyHex(),
|
|
0, blockchain.MinFee, mustJSON(blockchain.UnfollowPayload{}))
|
|
b3 := buildBlock(t, b2, val, []*blockchain.Transaction{unfollowTx})
|
|
mustAddBlock(t, c, b3)
|
|
|
|
following, _ = c.Following(alice.PubKeyHex())
|
|
if len(following) != 0 {
|
|
t.Errorf("Following after unfollow: got %v, want []", following)
|
|
}
|
|
}
|
|
|
|
// TestFeedLikeUnlike: like toggles + cached count stays consistent.
|
|
func TestFeedLikeUnlike(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t) // author
|
|
bob := newIdentity(t) // liker
|
|
host := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
|
|
const postSize = uint64(100)
|
|
expectedPostFee := blockchain.BasePostFee + postSize*blockchain.PostByteFee
|
|
fundAlice := makeTx(blockchain.EventTransfer, val.PubKeyHex(), alice.PubKeyHex(),
|
|
expectedPostFee+5*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
time.Sleep(2 * time.Millisecond)
|
|
fundBob := makeTx(blockchain.EventTransfer, val.PubKeyHex(), bob.PubKeyHex(),
|
|
5*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundAlice, fundBob})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
h := sha256.Sum256([]byte("likeable"))
|
|
postTx := makeTx(blockchain.EventCreatePost, alice.PubKeyHex(), "",
|
|
0, expectedPostFee,
|
|
mustJSON(blockchain.CreatePostPayload{
|
|
PostID: "p1", ContentHash: h[:], Size: postSize, HostingRelay: host.PubKeyHex(),
|
|
}))
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{postTx})
|
|
mustAddBlock(t, c, b2)
|
|
|
|
likeTx := makeTx(blockchain.EventLikePost, bob.PubKeyHex(), "",
|
|
0, blockchain.MinFee, mustJSON(blockchain.LikePostPayload{PostID: "p1"}))
|
|
b3 := buildBlock(t, b2, val, []*blockchain.Transaction{likeTx})
|
|
mustAddBlock(t, c, b3)
|
|
|
|
n, _ := c.LikeCount("p1")
|
|
if n != 1 {
|
|
t.Errorf("LikeCount after like: got %d, want 1", n)
|
|
}
|
|
liked, _ := c.HasLiked("p1", bob.PubKeyHex())
|
|
if !liked {
|
|
t.Errorf("HasLiked after like: got false")
|
|
}
|
|
|
|
// Duplicate like — tx is skipped; counter stays at 1.
|
|
dupTx := makeTx(blockchain.EventLikePost, bob.PubKeyHex(), "",
|
|
0, blockchain.MinFee, mustJSON(blockchain.LikePostPayload{PostID: "p1"}))
|
|
b4 := buildBlock(t, b3, val, []*blockchain.Transaction{dupTx})
|
|
mustAddBlock(t, c, b4)
|
|
if n2, _ := c.LikeCount("p1"); n2 != 1 {
|
|
t.Errorf("LikeCount after duplicate: got %d, want 1 (tx should have been skipped)", n2)
|
|
}
|
|
|
|
unlikeTx := makeTx(blockchain.EventUnlikePost, bob.PubKeyHex(), "",
|
|
0, blockchain.MinFee, mustJSON(blockchain.UnlikePostPayload{PostID: "p1"}))
|
|
b5 := buildBlock(t, b4, val, []*blockchain.Transaction{unlikeTx})
|
|
mustAddBlock(t, c, b5)
|
|
|
|
n, _ = c.LikeCount("p1")
|
|
if n != 0 {
|
|
t.Errorf("LikeCount after unlike: got %d, want 0", n)
|
|
}
|
|
}
|
|
|
|
// TestFeedDeletePostByOther: only the author may delete their post.
|
|
func TestFeedDeletePostByOther(t *testing.T) {
|
|
c := newChain(t)
|
|
val := newIdentity(t)
|
|
alice := newIdentity(t)
|
|
mallory := newIdentity(t) // tries to delete alice's post
|
|
host := newIdentity(t)
|
|
|
|
genesis := addGenesis(t, c, val)
|
|
const postSize = uint64(100)
|
|
fee := blockchain.BasePostFee + postSize*blockchain.PostByteFee
|
|
fundAlice := makeTx(blockchain.EventTransfer, val.PubKeyHex(), alice.PubKeyHex(),
|
|
fee+5*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
time.Sleep(2 * time.Millisecond)
|
|
fundMallory := makeTx(blockchain.EventTransfer, val.PubKeyHex(), mallory.PubKeyHex(),
|
|
5*blockchain.MinFee, blockchain.MinFee, mustJSON(blockchain.TransferPayload{}))
|
|
b1 := buildBlock(t, genesis, val, []*blockchain.Transaction{fundAlice, fundMallory})
|
|
mustAddBlock(t, c, b1)
|
|
|
|
h := sha256.Sum256([]byte("body"))
|
|
postTx := makeTx(blockchain.EventCreatePost, alice.PubKeyHex(), "", 0, fee,
|
|
mustJSON(blockchain.CreatePostPayload{
|
|
PostID: "p1", ContentHash: h[:], Size: postSize, HostingRelay: host.PubKeyHex(),
|
|
}))
|
|
b2 := buildBlock(t, b1, val, []*blockchain.Transaction{postTx})
|
|
mustAddBlock(t, c, b2)
|
|
|
|
// Mallory tries to delete alice's post — block commits, tx is skipped.
|
|
delTx := makeTx(blockchain.EventDeletePost, mallory.PubKeyHex(), "", 0, blockchain.MinFee,
|
|
mustJSON(blockchain.DeletePostPayload{PostID: "p1"}))
|
|
b3 := buildBlock(t, b2, val, []*blockchain.Transaction{delTx})
|
|
mustAddBlock(t, c, b3)
|
|
rec, _ := c.Post("p1")
|
|
if rec == nil || rec.Deleted {
|
|
t.Fatalf("post was deleted by non-author: %+v", rec)
|
|
}
|
|
}
|
|
|
|
// silence unused-import lint if fmt ever gets trimmed from the feed tests.
|
|
var _ = fmt.Sprintf
|