blockchain sandbox: UTXO chain, p2p mining, attacker node
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package p2p
|
||||
|
||||
import "time"
|
||||
|
||||
type Event struct {
|
||||
Type string `json:"type"` // "block_accept", "block_reject", "block_mine"
|
||||
Node string `json:"node"` // short ID
|
||||
BlockHash string `json:"block_hash"` // first 8 chars
|
||||
Height int `json:"height"`
|
||||
From string `json:"from"` // who sent it (short ID), empty if mined
|
||||
Ts int64 `json:"ts"` // unix ms
|
||||
TxCount int `json:"tx_count"`
|
||||
}
|
||||
|
||||
// Global event stream. Buffered so nodes never block.
|
||||
var Events = make(chan Event, 1000)
|
||||
|
||||
func emit(e Event) {
|
||||
e.Ts = time.Now().UnixMilli()
|
||||
select {
|
||||
case Events <- e:
|
||||
default: // drop if nobody listens
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"blockchain/chain"
|
||||
"blockchain/config"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type miner struct {
|
||||
hashDelay *time.Duration
|
||||
onMined func(*chain.Block, time.Duration)
|
||||
work chan *chain.Block
|
||||
}
|
||||
|
||||
func newMiner(hashDelay *time.Duration, onMined func(*chain.Block, time.Duration)) *miner {
|
||||
return &miner{
|
||||
hashDelay: hashDelay,
|
||||
onMined: onMined,
|
||||
work: make(chan *chain.Block, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitWork sends a new block template to the miner.
|
||||
// If the miner is busy, the previous job is cancelled automatically.
|
||||
func (m *miner) SubmitWork(block *chain.Block) {
|
||||
// drain old work if any
|
||||
select {
|
||||
case <-m.work:
|
||||
default:
|
||||
}
|
||||
m.work <- block
|
||||
}
|
||||
|
||||
func (m *miner) Run(stop <-chan struct{}) {
|
||||
var (
|
||||
cancel chan struct{}
|
||||
done chan bool
|
||||
t0 time.Time
|
||||
block *chain.Block
|
||||
)
|
||||
|
||||
for {
|
||||
select {
|
||||
case block = <-m.work:
|
||||
// cancel previous mining if running
|
||||
if cancel != nil {
|
||||
close(cancel)
|
||||
<-done
|
||||
}
|
||||
cancel = make(chan struct{})
|
||||
done = make(chan bool, 1)
|
||||
t0 = time.Now()
|
||||
go func(b *chain.Block, c chan struct{}, d chan bool) {
|
||||
d <- mine(b, c, *m.hashDelay)
|
||||
}(block, cancel, done)
|
||||
|
||||
case ok := <-done:
|
||||
cancel = nil
|
||||
done = nil
|
||||
if ok {
|
||||
m.onMined(block, time.Since(t0))
|
||||
}
|
||||
|
||||
case <-stop:
|
||||
if cancel != nil {
|
||||
close(cancel)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mine перебирает nonce пока хеш не начнётся с нужного кол-ва нулей.
|
||||
func mine(block *chain.Block, cancel chan struct{}, hashDelay time.Duration) bool {
|
||||
prefix := strings.Repeat("0", config.Default.Chain.Difficulty)
|
||||
base := block.HashBase()
|
||||
for {
|
||||
select {
|
||||
case <-cancel:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
data := base + strconv.Itoa(block.Nonce)
|
||||
h := sha256.Sum256([]byte(data))
|
||||
block.Hash = hex.EncodeToString(h[:])
|
||||
if strings.HasPrefix(block.Hash, prefix) {
|
||||
return true
|
||||
}
|
||||
block.Nonce++
|
||||
if hashDelay > 0 {
|
||||
time.Sleep(hashDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package p2p
|
||||
|
||||
import "blockchain/chain"
|
||||
|
||||
type Msg interface {
|
||||
msg()
|
||||
}
|
||||
|
||||
type BlockMsg struct {
|
||||
Block *chain.Block
|
||||
From Peer
|
||||
}
|
||||
|
||||
type TxMsg struct {
|
||||
Tx *chain.Transaction
|
||||
From Peer
|
||||
}
|
||||
|
||||
type GetBlocksMsg struct {
|
||||
Height int
|
||||
From Peer
|
||||
}
|
||||
|
||||
type BlocksMsg struct {
|
||||
Blocks []*chain.Block
|
||||
}
|
||||
|
||||
type GetPeersMsg struct {
|
||||
From Peer
|
||||
}
|
||||
|
||||
type PeersMsg struct {
|
||||
Peers []Peer
|
||||
}
|
||||
|
||||
func (BlockMsg) msg() {}
|
||||
func (TxMsg) msg() {}
|
||||
func (GetBlocksMsg) msg() {}
|
||||
func (BlocksMsg) msg() {}
|
||||
func (GetPeersMsg) msg() {}
|
||||
func (PeersMsg) msg() {}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"blockchain/chain"
|
||||
"blockchain/config"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// link represents a network connection between two peers.
|
||||
type link struct {
|
||||
send chan Msg
|
||||
latency time.Duration
|
||||
}
|
||||
|
||||
func newlink(target Peer, latency time.Duration) *link {
|
||||
l := &link{
|
||||
send: make(chan Msg, 50),
|
||||
latency: latency,
|
||||
}
|
||||
|
||||
if l.latency > 0 {
|
||||
panic("newlink: latency < 0")
|
||||
}
|
||||
|
||||
go func() {
|
||||
for msg := range l.send {
|
||||
jitter := time.Duration(rand.NormFloat64()*float64(l.latency)/4 + float64(l.latency))
|
||||
if jitter > 0 {
|
||||
time.Sleep(jitter)
|
||||
}
|
||||
target.Receive(msg)
|
||||
}
|
||||
}()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Wallet *chain.Wallet
|
||||
Chain *chain.Blockchain
|
||||
Mempool *chain.Mempool
|
||||
HashDelay time.Duration
|
||||
conn chan Msg
|
||||
syncCh chan Msg
|
||||
peers map[Peer]*link
|
||||
mx sync.Mutex
|
||||
miner *miner
|
||||
seen map[string]struct{}
|
||||
}
|
||||
|
||||
func NewNode() *Node {
|
||||
n := &Node{
|
||||
Wallet: chain.NewWallet(),
|
||||
Chain: chain.NewBlockchain(),
|
||||
Mempool: chain.NewMempool(),
|
||||
conn: make(chan Msg, 100),
|
||||
syncCh: make(chan Msg, 10),
|
||||
peers: make(map[Peer]*link),
|
||||
seen: make(map[string]struct{}),
|
||||
}
|
||||
n.miner = newMiner(&n.HashDelay, func(block *chain.Block, took time.Duration) {
|
||||
if n.Chain.AddBlock(block) {
|
||||
n.Mempool.RemoveMined(block)
|
||||
fmt.Printf("[%s] Mined block #%d (%d txs), hash: %s took=%v nonce=%d\n",
|
||||
n.ShortID(), block.Height, len(block.Transactions), block.Hash[:16],
|
||||
took.Round(time.Millisecond), block.Nonce)
|
||||
emit(Event{Type: "block_mine", Node: n.ShortID(), BlockHash: block.Hash[:8], Height: block.Height, TxCount: len(block.Transactions)})
|
||||
n.Broadcast(BlockMsg{Block: block, From: n})
|
||||
} else {
|
||||
fmt.Printf("[%s] MINE-REJECT #%d (%d txs) took=%v — mined valid PoW but AddBlock failed\n",
|
||||
n.ShortID(), block.Height, len(block.Transactions), took.Round(time.Millisecond))
|
||||
}
|
||||
n.submitWork()
|
||||
})
|
||||
return n
|
||||
}
|
||||
|
||||
// Receive implements Peer — delivers a network message.
|
||||
func (n *Node) Receive(msg Msg) {
|
||||
select {
|
||||
case n.conn <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// ReceiveSync implements Peer — delivers a sync message.
|
||||
func (n *Node) ReceiveSync(msg Msg) {
|
||||
select {
|
||||
case n.syncCh <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) submitWork() {
|
||||
last := n.Chain.LastBlock()
|
||||
txs := n.Mempool.CollectBlock(n.Wallet.PublicKey)
|
||||
block := chain.NewBlock(last.Height+1, txs, last.Hash)
|
||||
n.miner.SubmitWork(block)
|
||||
}
|
||||
|
||||
func (n *Node) ShortID() string {
|
||||
return n.Wallet.ShortAddress()
|
||||
}
|
||||
|
||||
func (n *Node) MarkSeen(hash string) bool {
|
||||
n.mx.Lock()
|
||||
defer n.mx.Unlock()
|
||||
|
||||
if _, ok := n.seen[hash]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
const maxSeen = 10000
|
||||
|
||||
if len(n.seen) >= maxSeen {
|
||||
n.seen = make(map[string]struct{})
|
||||
}
|
||||
|
||||
n.seen[hash] = struct{}{}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *Node) AddPeer(peer Peer) {
|
||||
if peer == n {
|
||||
return
|
||||
}
|
||||
n.mx.Lock()
|
||||
defer n.mx.Unlock()
|
||||
if len(n.peers) >= config.Default.P2P.MaxPeers {
|
||||
return
|
||||
}
|
||||
if _, exists := n.peers[peer]; exists {
|
||||
return
|
||||
}
|
||||
n.peers[peer] = newlink(peer, config.Default.P2P.NetDelay)
|
||||
}
|
||||
|
||||
func (n *Node) PeerIDs() []string {
|
||||
n.mx.Lock()
|
||||
defer n.mx.Unlock()
|
||||
ids := make([]string, 0, len(n.peers))
|
||||
for p := range n.peers {
|
||||
ids = append(ids, p.ShortID())
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (n *Node) snapshotPeers() []Peer {
|
||||
n.mx.Lock()
|
||||
defer n.mx.Unlock()
|
||||
peers := make([]Peer, 0, len(n.peers))
|
||||
for p := range n.peers {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
return peers
|
||||
}
|
||||
|
||||
func (n *Node) Send(to Peer, msg Msg) {
|
||||
n.mx.Lock()
|
||||
link, ok := n.peers[to]
|
||||
n.mx.Unlock()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case link.send <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) Broadcast(msg Msg) {
|
||||
n.mx.Lock()
|
||||
links := make([]*link, 0, len(n.peers))
|
||||
for _, l := range n.peers {
|
||||
links = append(links, l)
|
||||
}
|
||||
n.mx.Unlock()
|
||||
|
||||
for _, l := range links {
|
||||
select {
|
||||
case l.send <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) Run(stop chan struct{}) {
|
||||
go n.miner.Run(stop)
|
||||
n.submitWork()
|
||||
|
||||
discover := time.NewTicker(10 * time.Second)
|
||||
defer discover.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-discover.C:
|
||||
peers := n.snapshotPeers()
|
||||
if len(peers) > 0 {
|
||||
n.Send(peers[rand.Intn(len(peers))], GetPeersMsg{From: n})
|
||||
}
|
||||
case msg := <-n.conn:
|
||||
switch m := msg.(type) {
|
||||
case BlockMsg:
|
||||
if n.MarkSeen(m.Block.Hash) {
|
||||
continue
|
||||
}
|
||||
if n.Chain.AddBlock(m.Block) {
|
||||
n.Mempool.RemoveMined(m.Block)
|
||||
fromID := ""
|
||||
if m.From != nil {
|
||||
fromID = m.From.ShortID()
|
||||
}
|
||||
emit(Event{Type: "block_accept", Node: n.ShortID(), BlockHash: m.Block.Hash[:8], Height: m.Block.Height, From: fromID, TxCount: len(m.Block.Transactions)})
|
||||
n.Broadcast(BlockMsg{Block: m.Block, From: n})
|
||||
n.submitWork()
|
||||
} else if m.Block.Height > n.Chain.LastBlock().Height {
|
||||
fromID := ""
|
||||
if m.From != nil {
|
||||
fromID = m.From.ShortID()
|
||||
}
|
||||
emit(Event{Type: "block_reject", Node: n.ShortID(), BlockHash: m.Block.Hash[:8], Height: m.Block.Height, From: fromID})
|
||||
emit(Event{Type: "sync_request", Node: n.ShortID(), Height: n.Chain.LastBlock().Height, From: fromID})
|
||||
n.requestSync(m.From)
|
||||
}
|
||||
case TxMsg:
|
||||
if n.MarkSeen(m.Tx.TxID()) {
|
||||
continue
|
||||
}
|
||||
n.Mempool.Add(m.Tx)
|
||||
n.Broadcast(TxMsg{Tx: m.Tx, From: n})
|
||||
case GetPeersMsg:
|
||||
if m.From != nil {
|
||||
n.Send(m.From, PeersMsg{Peers: n.snapshotPeers()})
|
||||
}
|
||||
case PeersMsg:
|
||||
for _, p := range m.Peers {
|
||||
n.AddPeer(p)
|
||||
}
|
||||
}
|
||||
case msg := <-n.syncCh:
|
||||
switch m := msg.(type) {
|
||||
case GetBlocksMsg:
|
||||
blocks := n.Chain.BlocksAfterHeight(m.Height)
|
||||
if len(blocks) > 0 && m.From != nil {
|
||||
m.From.ReceiveSync(BlocksMsg{Blocks: blocks})
|
||||
}
|
||||
case BlocksMsg:
|
||||
oldHeight := n.Chain.LastBlock().Height
|
||||
if n.Chain.ReplaceBlocks(m.Blocks) {
|
||||
for _, b := range m.Blocks {
|
||||
n.Mempool.RemoveMined(b)
|
||||
}
|
||||
newHeight := n.Chain.LastBlock().Height
|
||||
emit(Event{Type: "reorg", Node: n.ShortID(), Height: newHeight, TxCount: newHeight - oldHeight})
|
||||
fmt.Printf("[%s] Synced chain to block #%d\n",
|
||||
n.ShortID(), newHeight)
|
||||
n.submitWork()
|
||||
}
|
||||
}
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) requestSync(peer Peer) {
|
||||
if peer == nil {
|
||||
return
|
||||
}
|
||||
height := max(0, n.Chain.LastBlock().Height-5)
|
||||
peer.ReceiveSync(GetBlocksMsg{Height: height, From: n})
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package p2p
|
||||
|
||||
// Peer is the interface that any network participant must implement.
|
||||
// Both honest nodes and attackers implement this — the network doesn't know the difference.
|
||||
type Peer interface {
|
||||
// Receive delivers a message to this peer (network layer).
|
||||
Receive(msg Msg)
|
||||
// ReceiveSync delivers a sync message (block sync protocol).
|
||||
ReceiveSync(msg Msg)
|
||||
// ShortID returns a short identifier for logging.
|
||||
ShortID() string
|
||||
}
|
||||
Reference in New Issue
Block a user