package main import ( "blockchain/chain" "blockchain/config" "blockchain/p2p" "bytes" "crypto/ed25519" "encoding/hex" "encoding/json" "fmt" "math" "math/rand" "net/http" "time" ) type API struct { nodes []*p2p.Node } func (api *API) findNode(address string) *p2p.Node { for _, node := range api.nodes { if node.Wallet.Address() == address { return node } } return nil } func (api *API) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/tx", api.handleTx) mux.HandleFunc("/balance", api.handleBalance) mux.HandleFunc("/chain", api.handleChain) mux.HandleFunc("/nodes", api.handleNodes) mux.HandleFunc("/topology", api.handleTopology) mux.HandleFunc("/events", api.handleEvents) mux.Handle("/", http.FileServer(http.Dir("web"))) return cors(mux) } func cors(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { return } next.ServeHTTP(w, r) }) } // balanceOf computes balance from a coin store snapshot. func balanceOf(coins chain.CoinStore, owner ed25519.PublicKey) int { sum := 0 for _, c := range coins { if bytes.Equal(c.Owner, owner) { sum += c.Amount } } return sum } // coinsFor returns unspent coins owned by the given public key. func coinsFor(coins chain.CoinStore, owner ed25519.PublicKey) []chain.UnspentCoin { var result []chain.UnspentCoin for _, c := range coins { if bytes.Equal(c.Owner, owner) { result = append(result, *c) } } return result } func (api *API) handleTx(w http.ResponseWriter, r *http.Request) { var req struct { From string `json:"from"` To string `json:"to"` Amount int `json:"amount"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), 400) return } node := api.findNode(req.From) if node == nil { http.Error(w, "unknown sender address", 404) return } toKey, err := hex.DecodeString(req.To) if err != nil { http.Error(w, "invalid recipient address", 400) return } snap := node.Chain.Snapshot() unspent := coinsFor(snap.Coins, node.Wallet.PublicKey) tx := chain.NewTransaction(node.Wallet, ed25519.PublicKey(toKey), req.Amount, unspent) if tx == nil { http.Error(w, "insufficient funds", 400) return } node.MarkSeen(tx.TxID()) node.Mempool.Add(tx) node.Broadcast(p2p.TxMsg{Tx: tx, From: node}) json.NewEncoder(w).Encode(map[string]any{ "status": "ok", "tx": tx.String(), }) } func (api *API) handleBalance(w http.ResponseWriter, r *http.Request) { addr := r.URL.Query().Get("addr") if addr == "" { http.Error(w, "addr required", 400) return } key, err := hex.DecodeString(addr) if err != nil { http.Error(w, "invalid address", 400) return } snap := api.nodes[0].Chain.Snapshot() json.NewEncoder(w).Encode(map[string]any{ "address": addr, "balance": balanceOf(snap.Coins, ed25519.PublicKey(key)), }) } func (api *API) handleChain(w http.ResponseWriter, r *http.Request) { type blockInfo struct { Index int `json:"index"` Hash string `json:"hash"` PrevHash string `json:"prev_hash"` Miner string `json:"miner"` Txs []string `json:"txs"` } idx := 0 if q := r.URL.Query().Get("node"); q != "" { fmt.Sscanf(q, "%d", &idx) if idx < 0 || idx >= len(api.nodes) { idx = 0 } } snap := api.nodes[idx].Chain.Snapshot() blocks := make([]blockInfo, len(snap.Blocks)) for i, b := range snap.Blocks { txs := make([]string, len(b.Transactions)) miner := "" for j, tx := range b.Transactions { txs[j] = tx.String() if tx.IsCoinbase() && len(tx.Coins) > 0 { miner = tx.Coins[0].OwnerHex() } } blocks[i] = blockInfo{b.Height, b.Hash, b.PrevHash, miner, txs} } json.NewEncoder(w).Encode(blocks) } func (api *API) handleNodes(w http.ResponseWriter, r *http.Request) { type nodeInfo struct { Address string `json:"address"` Balance int `json:"balance"` HashDelay int `json:"hash_delay_us"` ChainLen int `json:"chain_len"` LastHash string `json:"last_hash"` PendingTxs int `json:"pending_txs"` } nodes := make([]nodeInfo, len(api.nodes)) for i, n := range api.nodes { snap := n.Chain.Snapshot() last := snap.Blocks[len(snap.Blocks)-1] nodes[i] = nodeInfo{ Address: n.Wallet.Address(), Balance: balanceOf(snap.Coins, n.Wallet.PublicKey), HashDelay: int(n.HashDelay.Microseconds()), ChainLen: len(snap.Blocks), LastHash: last.Hash, PendingTxs: n.Mempool.Len(), } } json.NewEncoder(w).Encode(nodes) } func (api *API) handleTopology(w http.ResponseWriter, r *http.Request) { type topoNode struct { ID string `json:"id"` Peers []string `json:"peers"` HashDelay int `json:"hash_delay_us"` } result := make([]topoNode, len(api.nodes)) for i, n := range api.nodes { result[i] = topoNode{ ID: n.ShortID(), Peers: n.PeerIDs(), HashDelay: int(n.HashDelay.Microseconds()), } } json.NewEncoder(w).Encode(result) } func (api *API) handleEvents(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming not supported", 500) return } for { select { case ev := <-p2p.Events: data, _ := json.Marshal(ev) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() case <-r.Context().Done(): return } } } func main() { // --- simulation parameters (Bitcoin-proportional) --- cfg := &config.Default numNodes := cfg.Sim.NumNodes hashDelay := cfg.Sim.HashDelay expectedBlock := time.Duration(float64(hashDelay) * math.Pow(16, float64(cfg.Chain.Difficulty)) / float64(numNodes)) cfg.P2P.NetDelay = expectedBlock / 300 fmt.Printf("Difficulty=%d Nodes=%d HashDelay=%v OutboundPeers=%d\n", cfg.Chain.Difficulty, numNodes, hashDelay, cfg.P2P.OutboundPeers) fmt.Printf("Expected ~%v per block, net delay ~%v\n\n", expectedBlock.Round(time.Millisecond), cfg.P2P.NetDelay.Round(time.Microsecond)) stop := make(chan struct{}) nodes := make([]*p2p.Node, numNodes) for i := range nodes { nodes[i] = p2p.NewNode() r := rand.Float64() switch { case r < 0.2: nodes[i].HashDelay = hashDelay / 3 case r < 0.8: nodes[i].HashDelay = hashDelay default: nodes[i].HashDelay = hashDelay * 3 } for _, j := range rand.Perm(i)[:min(cfg.P2P.OutboundPeers, i)] { nodes[i].AddPeer(nodes[j]) nodes[j].AddPeer(nodes[i]) } go nodes[i].Run(stop) } fmt.Printf("%d nodes started\n", numNodes) // bot: randomly send transactions go func() { for { time.Sleep(time.Duration(250+rand.Intn(250)) * time.Millisecond) from := nodes[rand.Intn(len(nodes))] to := nodes[rand.Intn(len(nodes))] if from == to { continue } snap := from.Chain.Snapshot() unspent := coinsFor(snap.Coins, from.Wallet.PublicKey) balance := 0 for _, u := range unspent { balance += u.Amount } if balance <= 0 { continue } amount := 1 + rand.Intn(min(balance, 10)) tx := chain.NewTransaction(from.Wallet, to.Wallet.PublicKey, amount, unspent) if tx == nil { continue } from.MarkSeen(tx.TxID()) from.Mempool.Add(tx) p2p.Events <- p2p.Event{Type: "tx_send", Node: from.ShortID(), From: from.ShortID(), BlockHash: to.ShortID(), TxCount: amount, Ts: time.Now().UnixMilli()} from.Broadcast(p2p.TxMsg{Tx: tx, From: from}) } }() // periodic network health stats go func() { for { time.Sleep(10 * time.Second) heights := map[int]int{} tips := map[string]int{} for _, n := range nodes { last := n.Chain.LastBlock() heights[last.Height]++ tips[last.Hash[:8]]++ } fmt.Printf("\n--- NETWORK STATS: heights=%v tips=%v ---\n\n", heights, tips) } }() api := &API{nodes: nodes} fmt.Println() fmt.Println("=== http://localhost:8080 ===") fmt.Println() fmt.Println(" POST /tx — {from, to, amount}") fmt.Println(" GET /balance?addr=... — check balance") fmt.Println(" GET /chain — view blockchain") fmt.Println(" GET /nodes — list all nodes") if err := http.ListenAndServe(":8080", api.Handler()); err != nil { fmt.Println("Error:", err) } }