blockchain sandbox: UTXO chain, p2p mining, attacker node
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"blockchain/attacker"
|
||||
"blockchain/config"
|
||||
"blockchain/p2p"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
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("Starting honest network: %d nodes, difficulty=%d\n", numNodes, cfg.Chain.Difficulty)
|
||||
|
||||
// 1. Start honest network
|
||||
stop := make(chan struct{})
|
||||
nodes := make([]*p2p.Node, numNodes)
|
||||
for i := range nodes {
|
||||
nodes[i] = p2p.NewNode()
|
||||
nodes[i].HashDelay = hashDelay
|
||||
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)
|
||||
}
|
||||
|
||||
// Let the network mine a few blocks first
|
||||
fmt.Println("Waiting for network to mine some blocks...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// 2. Create attacker node and connect to network
|
||||
evil := attacker.NewNode()
|
||||
evil.HashDelay = hashDelay
|
||||
|
||||
// Connect to a few honest nodes (like joining the network)
|
||||
for _, j := range rand.Perm(len(nodes))[:min(3, len(nodes))] {
|
||||
evil.AddPeer(nodes[j])
|
||||
nodes[j].AddPeer(evil)
|
||||
}
|
||||
|
||||
fmt.Printf("\n[ATTACKER] Joined network as %s\n", evil.ShortID())
|
||||
fmt.Printf("[ATTACKER] Modify attacker/node.go Run() to implement your attack!\n\n")
|
||||
|
||||
go evil.Run(stop)
|
||||
|
||||
// Wait
|
||||
select {}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user