blockchain sandbox: UTXO chain, p2p mining, attacker node
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"blockchain/config"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Block struct {
|
||||
Height int
|
||||
Transactions []*Transaction
|
||||
PrevHash string
|
||||
Hash string
|
||||
Nonce int
|
||||
}
|
||||
|
||||
func NewBlock(height int, txs []*Transaction, prevHash string) *Block {
|
||||
return &Block{
|
||||
Height: height,
|
||||
Transactions: txs,
|
||||
PrevHash: prevHash,
|
||||
}
|
||||
}
|
||||
|
||||
// HashBase returns the pre-computed prefix for nonce iteration.
|
||||
func (b *Block) HashBase() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(strconv.Itoa(b.Height))
|
||||
for _, tx := range b.Transactions {
|
||||
sb.WriteString(tx.TxID())
|
||||
}
|
||||
sb.WriteString(b.PrevHash)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (b *Block) CalcHash() string {
|
||||
data := b.HashBase() + strconv.Itoa(b.Nonce)
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func (b *Block) CheckProofOfWork() bool {
|
||||
prefix := strings.Repeat("0", config.Default.Chain.Difficulty)
|
||||
return b.Hash == b.CalcHash() && strings.HasPrefix(b.Hash, prefix)
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"blockchain/config"
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Snapshot is a read-only view of the chain state for API queries.
|
||||
|
||||
type Blockchain struct {
|
||||
blocks []*Block
|
||||
coins CoinStore
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
func NewBlockchain() *Blockchain {
|
||||
genesis := NewBlock(0, nil, "")
|
||||
genesis.Hash = genesis.CalcHash()
|
||||
return &Blockchain{
|
||||
blocks: []*Block{genesis},
|
||||
coins: make(CoinStore),
|
||||
}
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Blocks []*Block
|
||||
Coins CoinStore
|
||||
}
|
||||
|
||||
// Snapshot returns a consistent read-only view of the chain state.
|
||||
func (cs *Blockchain) Snapshot() Snapshot {
|
||||
cs.mx.RLock()
|
||||
defer cs.mx.RUnlock()
|
||||
|
||||
blocks := make([]*Block, len(cs.blocks))
|
||||
copy(blocks, cs.blocks)
|
||||
|
||||
return Snapshot{
|
||||
Blocks: blocks,
|
||||
Coins: cs.coins,
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *Blockchain) LastBlock() *Block {
|
||||
cs.mx.RLock()
|
||||
defer cs.mx.RUnlock()
|
||||
|
||||
return cs.blocks[len(cs.blocks)-1]
|
||||
}
|
||||
|
||||
func (cs *Blockchain) AddBlock(b *Block) bool {
|
||||
cs.mx.Lock()
|
||||
defer cs.mx.Unlock()
|
||||
|
||||
return cs.addBlock(b)
|
||||
}
|
||||
|
||||
// addBlock validates and appends a block. Caller must hold write lock.
|
||||
func (cs *Blockchain) addBlock(b *Block) bool {
|
||||
last := cs.blocks[len(cs.blocks)-1]
|
||||
if b.PrevHash != last.Hash || b.Height != last.Height+1 {
|
||||
return false
|
||||
}
|
||||
if !b.CheckProofOfWork() {
|
||||
return false
|
||||
}
|
||||
if !cs.validateTransactions(b.Transactions) {
|
||||
return false
|
||||
}
|
||||
cs.applyBlock(b)
|
||||
cs.blocks = append(cs.blocks, b)
|
||||
return true
|
||||
}
|
||||
|
||||
func (cs *Blockchain) validateTransactions(txs []*Transaction) bool {
|
||||
working := cs.coins.Clone()
|
||||
coinbaseCount := 0
|
||||
|
||||
for _, tx := range txs {
|
||||
if tx.IsCoinbase() {
|
||||
coinbaseCount++
|
||||
if coinbaseCount > 1 {
|
||||
return false
|
||||
}
|
||||
if len(tx.Coins) != 1 || tx.Coins[0].Amount != config.Default.Chain.MiningReward {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !cs.validateTx(tx, working) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, s := range tx.Spends {
|
||||
working.Spend(s.CoinID)
|
||||
}
|
||||
txID := tx.TxID()
|
||||
for i, c := range tx.Coins {
|
||||
working.Add(txID, i, c)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (cs *Blockchain) validateTx(tx *Transaction, coins CoinStore) bool {
|
||||
if !tx.Verify() {
|
||||
return false
|
||||
}
|
||||
|
||||
inputSum := 0
|
||||
for _, s := range tx.Spends {
|
||||
coin := coins.Get(s.CoinID)
|
||||
if coin == nil {
|
||||
return false
|
||||
}
|
||||
if !bytes.Equal(coin.Owner, s.Owner) {
|
||||
return false
|
||||
}
|
||||
inputSum += coin.Amount
|
||||
}
|
||||
|
||||
outputSum := 0
|
||||
for _, c := range tx.Coins {
|
||||
if c.Amount <= 0 {
|
||||
return false
|
||||
}
|
||||
outputSum += c.Amount
|
||||
}
|
||||
|
||||
return outputSum <= inputSum
|
||||
}
|
||||
|
||||
func (cs *Blockchain) applyBlock(b *Block) {
|
||||
for _, tx := range b.Transactions {
|
||||
for _, s := range tx.Spends {
|
||||
cs.coins.Spend(s.CoinID)
|
||||
}
|
||||
txID := tx.TxID()
|
||||
for i, c := range tx.Coins {
|
||||
cs.coins.Add(txID, i, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *Blockchain) rebuildCoins() {
|
||||
cs.coins = make(CoinStore)
|
||||
for _, b := range cs.blocks {
|
||||
cs.applyBlock(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *Blockchain) ReplaceBlocks(blocks []*Block) bool {
|
||||
cs.mx.Lock()
|
||||
defer cs.mx.Unlock()
|
||||
|
||||
if len(blocks) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
first := blocks[0]
|
||||
attachIdx := -1
|
||||
for i, b := range cs.blocks {
|
||||
if b.Hash == first.PrevHash {
|
||||
attachIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if attachIdx == -1 {
|
||||
return false
|
||||
}
|
||||
if attachIdx+1+len(blocks) <= len(cs.blocks) {
|
||||
return false
|
||||
}
|
||||
|
||||
oldBlocks := make([]*Block, len(cs.blocks))
|
||||
copy(oldBlocks, cs.blocks)
|
||||
oldCoins := cs.coins
|
||||
|
||||
cs.blocks = cs.blocks[:attachIdx+1]
|
||||
cs.rebuildCoins()
|
||||
|
||||
for _, b := range blocks {
|
||||
if !cs.addBlock(b) {
|
||||
cs.blocks = oldBlocks
|
||||
cs.coins = oldCoins
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BlocksAfterHeight returns all blocks after the given height.
|
||||
func (cs *Blockchain) BlocksAfterHeight(height int) []*Block {
|
||||
cs.mx.RLock()
|
||||
defer cs.mx.RUnlock()
|
||||
if height+1 >= len(cs.blocks) {
|
||||
return nil
|
||||
}
|
||||
result := make([]*Block, len(cs.blocks)-height-1)
|
||||
copy(result, cs.blocks[height+1:])
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"blockchain/config"
|
||||
"crypto/ed25519"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Mempool struct {
|
||||
transations []*Transaction
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func NewMempool() *Mempool {
|
||||
return &Mempool{}
|
||||
}
|
||||
|
||||
func (m *Mempool) Add(tx *Transaction) {
|
||||
if !tx.Verify() {
|
||||
return
|
||||
}
|
||||
m.mx.Lock()
|
||||
m.transations = append(m.transations, tx)
|
||||
m.mx.Unlock()
|
||||
}
|
||||
|
||||
func (m *Mempool) Len() int {
|
||||
m.mx.Lock()
|
||||
defer m.mx.Unlock()
|
||||
|
||||
return len(m.transations)
|
||||
}
|
||||
|
||||
func (m *Mempool) CollectBlock(miner ed25519.PublicKey) []*Transaction {
|
||||
m.mx.Lock()
|
||||
defer m.mx.Unlock()
|
||||
|
||||
txs := []*Transaction{NewCoinbaseTransaction(miner, config.Default.Chain.MiningReward)}
|
||||
spent := make(map[string]struct{})
|
||||
|
||||
isSpent := func(s CoinSpend) bool {
|
||||
_, ok := spent[s.CoinID.Key()]
|
||||
return ok
|
||||
}
|
||||
|
||||
for _, tx := range m.transations {
|
||||
if slices.ContainsFunc(tx.Spends, isSpent) {
|
||||
continue
|
||||
}
|
||||
|
||||
txs = append(txs, tx)
|
||||
|
||||
for _, s := range tx.Spends {
|
||||
spent[s.CoinID.Key()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return txs
|
||||
}
|
||||
|
||||
func (m *Mempool) RemoveMined(block *Block) {
|
||||
spent := make(map[string]struct{})
|
||||
for _, tx := range block.Transactions {
|
||||
if tx.IsCoinbase() {
|
||||
continue
|
||||
}
|
||||
for _, s := range tx.Spends {
|
||||
spent[s.CoinID.Key()] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(spent) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.mx.Lock()
|
||||
defer m.mx.Unlock()
|
||||
|
||||
isSpent := func(s CoinSpend) bool {
|
||||
_, ok := spent[s.CoinID.Key()]
|
||||
return ok
|
||||
}
|
||||
|
||||
m.transations = slices.DeleteFunc(m.transations, func(tx *Transaction) bool {
|
||||
return slices.ContainsFunc(tx.Spends, isSpent)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CoinSpend references a coin being spent, with proof of ownership.
|
||||
type CoinSpend struct {
|
||||
CoinID CoinID
|
||||
Owner ed25519.PublicKey
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
type Transaction struct {
|
||||
Spends []CoinSpend
|
||||
Coins []Coin
|
||||
|
||||
cachedID string
|
||||
}
|
||||
|
||||
func (tx *Transaction) IsCoinbase() bool {
|
||||
return len(tx.Spends) == 0
|
||||
}
|
||||
|
||||
func (tx *Transaction) TxID() string {
|
||||
if tx.cachedID != "" {
|
||||
return tx.cachedID
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
for _, s := range tx.Spends {
|
||||
b.WriteString(s.CoinID.TxHash)
|
||||
b.WriteString(strconv.Itoa(s.CoinID.Index))
|
||||
}
|
||||
for _, c := range tx.Coins {
|
||||
b.Write(c.Owner)
|
||||
b.WriteString(strconv.Itoa(c.Amount))
|
||||
}
|
||||
h := sha256.Sum256([]byte(b.String()))
|
||||
tx.cachedID = hex.EncodeToString(h[:])
|
||||
return tx.cachedID
|
||||
}
|
||||
|
||||
func (tx *Transaction) Verify() bool {
|
||||
if tx.IsCoinbase() {
|
||||
return true
|
||||
}
|
||||
|
||||
hash := []byte(tx.TxID())
|
||||
for _, s := range tx.Spends {
|
||||
if s.Owner == nil {
|
||||
return false
|
||||
}
|
||||
if !ed25519.Verify(s.Owner, hash, s.Signature) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (tx *Transaction) String() string {
|
||||
if tx.IsCoinbase() {
|
||||
return fmt.Sprintf("COINBASE -> %s: %d", tx.Coins[0].OwnerHex()[:7], tx.Coins[0].Amount)
|
||||
}
|
||||
|
||||
from := hex.EncodeToString(tx.Spends[0].Owner)[:7]
|
||||
to := tx.Coins[0].OwnerHex()[:7]
|
||||
|
||||
return fmt.Sprintf("%s -> %s: %d", from, to, tx.Coins[0].Amount)
|
||||
}
|
||||
|
||||
func NewCoinbaseTransaction(to ed25519.PublicKey, amount int) *Transaction {
|
||||
return &Transaction{
|
||||
Coins: []Coin{{Amount: amount, Owner: to}},
|
||||
}
|
||||
}
|
||||
|
||||
func NewTransaction(wallet *Wallet, to ed25519.PublicKey, amount int, unspent []UnspentCoin) *Transaction {
|
||||
var selected []UnspentCoin
|
||||
var total int
|
||||
|
||||
for _, u := range unspent {
|
||||
selected = append(selected, u)
|
||||
total += u.Amount
|
||||
if total >= amount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if total < amount {
|
||||
return nil
|
||||
}
|
||||
|
||||
coins := []Coin{{Amount: amount, Owner: to}}
|
||||
|
||||
change := total - amount
|
||||
|
||||
if change > 0 {
|
||||
coins = append(coins, Coin{Amount: change, Owner: wallet.PublicKey})
|
||||
}
|
||||
|
||||
spends := make([]CoinSpend, len(selected))
|
||||
for i, u := range selected {
|
||||
spends[i] = CoinSpend{
|
||||
CoinID: u.CoinID,
|
||||
Owner: wallet.PublicKey,
|
||||
}
|
||||
}
|
||||
|
||||
tx := &Transaction{Spends: spends, Coins: coins}
|
||||
hash := []byte(tx.TxID())
|
||||
for i := range tx.Spends {
|
||||
tx.Spends[i].Signature = ed25519.Sign(wallet.PrivateKey, hash)
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"maps"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type CoinID struct {
|
||||
TxHash string
|
||||
Index int
|
||||
}
|
||||
|
||||
func (id CoinID) Key() string { return id.TxHash + ":" + strconv.Itoa(id.Index) }
|
||||
|
||||
type Coin struct {
|
||||
Amount int
|
||||
Owner ed25519.PublicKey
|
||||
}
|
||||
|
||||
func (c Coin) OwnerHex() string { return hex.EncodeToString(c.Owner) }
|
||||
|
||||
type UnspentCoin struct {
|
||||
Coin
|
||||
CoinID CoinID
|
||||
}
|
||||
|
||||
type CoinStore map[string]*UnspentCoin
|
||||
|
||||
func (s CoinStore) Add(txHash string, idx int, coin Coin) {
|
||||
id := CoinID{TxHash: txHash, Index: idx}
|
||||
s[id.Key()] = &UnspentCoin{Coin: coin, CoinID: id}
|
||||
}
|
||||
|
||||
func (s CoinStore) Spend(id CoinID) { delete(s, id.Key()) }
|
||||
|
||||
func (s CoinStore) Get(id CoinID) *UnspentCoin { return s[id.Key()] }
|
||||
|
||||
func (s CoinStore) Clone() CoinStore {
|
||||
c := make(CoinStore, len(s))
|
||||
maps.Copy(c, s)
|
||||
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
type Wallet struct {
|
||||
PrivateKey ed25519.PrivateKey
|
||||
PublicKey ed25519.PublicKey
|
||||
}
|
||||
|
||||
func NewWallet() *Wallet {
|
||||
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
return &Wallet{PrivateKey: priv, PublicKey: pub}
|
||||
}
|
||||
|
||||
func (w *Wallet) Address() string {
|
||||
return hex.EncodeToString(w.PublicKey)
|
||||
}
|
||||
|
||||
func (w *Wallet) ShortAddress() string {
|
||||
return w.Address()[:7]
|
||||
}
|
||||
Reference in New Issue
Block a user