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
This commit is contained in:
796
blockchain/chain_test.go
Normal file
796
blockchain/chain_test.go
Normal file
@@ -0,0 +1,796 @@
|
||||
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
|
||||
Reference in New Issue
Block a user