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