forked from cerc-io/plugeth
commit
df2b70853f
@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/ethereum/eth-go/ethcrypto"
|
"github.com/ethereum/eth-go/ethcrypto"
|
||||||
"github.com/ethereum/eth-go/ethlog"
|
"github.com/ethereum/eth-go/ethlog"
|
||||||
"github.com/ethereum/eth-go/ethreact"
|
|
||||||
"github.com/ethereum/eth-go/ethutil"
|
"github.com/ethereum/eth-go/ethutil"
|
||||||
"github.com/obscuren/sha3"
|
"github.com/obscuren/sha3"
|
||||||
)
|
)
|
||||||
@ -16,7 +15,7 @@ import (
|
|||||||
var powlogger = ethlog.NewLogger("POW")
|
var powlogger = ethlog.NewLogger("POW")
|
||||||
|
|
||||||
type PoW interface {
|
type PoW interface {
|
||||||
Search(block *Block, reactChan chan ethreact.Event) []byte
|
Search(block *Block, stop <-chan struct{}) []byte
|
||||||
Verify(hash []byte, diff *big.Int, nonce []byte) bool
|
Verify(hash []byte, diff *big.Int, nonce []byte) bool
|
||||||
GetHashrate() int64
|
GetHashrate() int64
|
||||||
Turbo(bool)
|
Turbo(bool)
|
||||||
@ -36,7 +35,7 @@ func (pow *EasyPow) Turbo(on bool) {
|
|||||||
pow.turbo = on
|
pow.turbo = on
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pow *EasyPow) Search(block *Block, reactChan chan ethreact.Event) []byte {
|
func (pow *EasyPow) Search(block *Block, stop <-chan struct{}) []byte {
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
hash := block.HashNoNonce()
|
hash := block.HashNoNonce()
|
||||||
diff := block.Difficulty
|
diff := block.Difficulty
|
||||||
@ -46,7 +45,7 @@ func (pow *EasyPow) Search(block *Block, reactChan chan ethreact.Event) []byte {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-reactChan:
|
case <-stop:
|
||||||
powlogger.Infoln("Breaking from mining")
|
powlogger.Infoln("Breaking from mining")
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
|
10
ethchain/events.go
Normal file
10
ethchain/events.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package ethchain
|
||||||
|
|
||||||
|
type TxEvent struct {
|
||||||
|
Type int // TxPre || TxPost
|
||||||
|
Tx *Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewBlockEvent struct {
|
||||||
|
Block *Block
|
||||||
|
}
|
@ -3,5 +3,5 @@ package ethchain
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestFilter(t *testing.T) {
|
func TestFilter(t *testing.T) {
|
||||||
filter := NewFilter()
|
NewFilter(NewTestManager())
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,17 @@ import (
|
|||||||
|
|
||||||
"github.com/ethereum/eth-go/ethcrypto"
|
"github.com/ethereum/eth-go/ethcrypto"
|
||||||
"github.com/ethereum/eth-go/ethdb"
|
"github.com/ethereum/eth-go/ethdb"
|
||||||
"github.com/ethereum/eth-go/ethreact"
|
|
||||||
"github.com/ethereum/eth-go/ethutil"
|
"github.com/ethereum/eth-go/ethutil"
|
||||||
"github.com/ethereum/eth-go/ethwire"
|
"github.com/ethereum/eth-go/ethwire"
|
||||||
|
"github.com/ethereum/eth-go/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implement our EthTest Manager
|
// Implement our EthTest Manager
|
||||||
type TestManager struct {
|
type TestManager struct {
|
||||||
stateManager *StateManager
|
stateManager *StateManager
|
||||||
reactor *ethreact.ReactorEngine
|
eventMux *event.TypeMux
|
||||||
|
|
||||||
|
db ethutil.Database
|
||||||
txPool *TxPool
|
txPool *TxPool
|
||||||
blockChain *BlockChain
|
blockChain *BlockChain
|
||||||
Blocks []*Block
|
Blocks []*Block
|
||||||
@ -49,8 +50,8 @@ func (tm *TestManager) StateManager() *StateManager {
|
|||||||
return tm.stateManager
|
return tm.stateManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TestManager) Reactor() *ethreact.ReactorEngine {
|
func (tm *TestManager) EventMux() *event.TypeMux {
|
||||||
return tm.reactor
|
return tm.eventMux
|
||||||
}
|
}
|
||||||
func (tm *TestManager) Broadcast(msgType ethwire.MsgType, data []interface{}) {
|
func (tm *TestManager) Broadcast(msgType ethwire.MsgType, data []interface{}) {
|
||||||
fmt.Println("Broadcast not implemented")
|
fmt.Println("Broadcast not implemented")
|
||||||
@ -63,7 +64,10 @@ func (tm *TestManager) KeyManager() *ethcrypto.KeyManager {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TestManager) Db() ethutil.Database { return nil }
|
func (tm *TestManager) Db() ethutil.Database {
|
||||||
|
return tm.db
|
||||||
|
}
|
||||||
|
|
||||||
func NewTestManager() *TestManager {
|
func NewTestManager() *TestManager {
|
||||||
ethutil.ReadConfig(".ethtest", "/tmp/ethtest", "ETH")
|
ethutil.ReadConfig(".ethtest", "/tmp/ethtest", "ETH")
|
||||||
|
|
||||||
@ -75,8 +79,8 @@ func NewTestManager() *TestManager {
|
|||||||
ethutil.Config.Db = db
|
ethutil.Config.Db = db
|
||||||
|
|
||||||
testManager := &TestManager{}
|
testManager := &TestManager{}
|
||||||
testManager.reactor = ethreact.New()
|
testManager.eventMux = new(event.TypeMux)
|
||||||
|
testManager.db = db
|
||||||
testManager.txPool = NewTxPool(testManager)
|
testManager.txPool = NewTxPool(testManager)
|
||||||
testManager.blockChain = NewBlockChain(testManager)
|
testManager.blockChain = NewBlockChain(testManager)
|
||||||
testManager.stateManager = NewStateManager(testManager)
|
testManager.stateManager = NewStateManager(testManager)
|
||||||
|
@ -11,11 +11,10 @@ import (
|
|||||||
|
|
||||||
"github.com/ethereum/eth-go/ethcrypto"
|
"github.com/ethereum/eth-go/ethcrypto"
|
||||||
"github.com/ethereum/eth-go/ethlog"
|
"github.com/ethereum/eth-go/ethlog"
|
||||||
"github.com/ethereum/eth-go/ethreact"
|
|
||||||
"github.com/ethereum/eth-go/ethstate"
|
"github.com/ethereum/eth-go/ethstate"
|
||||||
"github.com/ethereum/eth-go/ethutil"
|
"github.com/ethereum/eth-go/ethutil"
|
||||||
"github.com/ethereum/eth-go/ethwire"
|
"github.com/ethereum/eth-go/ethwire"
|
||||||
"github.com/ethereum/eth-go/eventer"
|
"github.com/ethereum/eth-go/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
var statelogger = ethlog.NewLogger("STATE")
|
var statelogger = ethlog.NewLogger("STATE")
|
||||||
@ -37,7 +36,6 @@ type EthManager interface {
|
|||||||
BlockChain() *BlockChain
|
BlockChain() *BlockChain
|
||||||
TxPool() *TxPool
|
TxPool() *TxPool
|
||||||
Broadcast(msgType ethwire.MsgType, data []interface{})
|
Broadcast(msgType ethwire.MsgType, data []interface{})
|
||||||
Reactor() *ethreact.ReactorEngine
|
|
||||||
PeerCount() int
|
PeerCount() int
|
||||||
IsMining() bool
|
IsMining() bool
|
||||||
IsListening() bool
|
IsListening() bool
|
||||||
@ -45,7 +43,7 @@ type EthManager interface {
|
|||||||
KeyManager() *ethcrypto.KeyManager
|
KeyManager() *ethcrypto.KeyManager
|
||||||
ClientIdentity() ethwire.ClientIdentity
|
ClientIdentity() ethwire.ClientIdentity
|
||||||
Db() ethutil.Database
|
Db() ethutil.Database
|
||||||
Eventer() *eventer.EventMachine
|
EventMux() *event.TypeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateManager struct {
|
type StateManager struct {
|
||||||
@ -73,8 +71,7 @@ type StateManager struct {
|
|||||||
// 'Process' & canonical validation.
|
// 'Process' & canonical validation.
|
||||||
lastAttemptedBlock *Block
|
lastAttemptedBlock *Block
|
||||||
|
|
||||||
// Quit chan
|
events event.Subscription
|
||||||
quit chan bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStateManager(ethereum EthManager) *StateManager {
|
func NewStateManager(ethereum EthManager) *StateManager {
|
||||||
@ -83,7 +80,6 @@ func NewStateManager(ethereum EthManager) *StateManager {
|
|||||||
Pow: &EasyPow{},
|
Pow: &EasyPow{},
|
||||||
eth: ethereum,
|
eth: ethereum,
|
||||||
bc: ethereum.BlockChain(),
|
bc: ethereum.BlockChain(),
|
||||||
quit: make(chan bool),
|
|
||||||
}
|
}
|
||||||
sm.transState = ethereum.BlockChain().CurrentBlock.State().Copy()
|
sm.transState = ethereum.BlockChain().CurrentBlock.State().Copy()
|
||||||
sm.miningState = ethereum.BlockChain().CurrentBlock.State().Copy()
|
sm.miningState = ethereum.BlockChain().CurrentBlock.State().Copy()
|
||||||
@ -93,25 +89,18 @@ func NewStateManager(ethereum EthManager) *StateManager {
|
|||||||
|
|
||||||
func (self *StateManager) Start() {
|
func (self *StateManager) Start() {
|
||||||
statelogger.Debugln("Starting state manager")
|
statelogger.Debugln("Starting state manager")
|
||||||
|
self.events = self.eth.EventMux().Subscribe(Blocks(nil))
|
||||||
go self.updateThread()
|
go self.updateThread()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *StateManager) Stop() {
|
func (self *StateManager) Stop() {
|
||||||
statelogger.Debugln("Stopping state manager")
|
statelogger.Debugln("Stopping state manager")
|
||||||
|
self.events.Unsubscribe()
|
||||||
close(self.quit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *StateManager) updateThread() {
|
func (self *StateManager) updateThread() {
|
||||||
blockChan := self.eth.Eventer().Register("blocks")
|
for ev := range self.events.Chan() {
|
||||||
|
for _, block := range ev.(Blocks) {
|
||||||
out:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-blockChan:
|
|
||||||
blocks := event.Data.(Blocks)
|
|
||||||
for _, block := range blocks {
|
|
||||||
err := self.Process(block, false)
|
err := self.Process(block, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statelogger.Infoln(err)
|
statelogger.Infoln(err)
|
||||||
@ -120,10 +109,6 @@ out:
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-self.quit:
|
|
||||||
break out
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,7 +187,7 @@ done:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify all subscribers
|
// Notify all subscribers
|
||||||
self.eth.Reactor().Post("newTx:post", tx)
|
self.eth.EventMux().Post(TxEvent{TxPost, tx})
|
||||||
|
|
||||||
receipts = append(receipts, receipt)
|
receipts = append(receipts, receipt)
|
||||||
handled = append(handled, tx)
|
handled = append(handled, tx)
|
||||||
@ -293,7 +278,7 @@ func (sm *StateManager) Process(block *Block, dontReact bool) (err error) {
|
|||||||
|
|
||||||
statelogger.Infof("Imported block #%d (%x...)\n", block.Number, block.Hash()[0:4])
|
statelogger.Infof("Imported block #%d (%x...)\n", block.Number, block.Hash()[0:4])
|
||||||
if dontReact == false {
|
if dontReact == false {
|
||||||
sm.eth.Reactor().Post("newBlock", block)
|
sm.eth.EventMux().Post(NewBlockEvent{block})
|
||||||
|
|
||||||
state.Manifest().Reset()
|
state.Manifest().Reset()
|
||||||
}
|
}
|
||||||
@ -434,7 +419,7 @@ func (sm *StateManager) createBloomFilter(state *ethstate.State) *BloomFilter {
|
|||||||
bloomf.Set(msg.From)
|
bloomf.Set(msg.From)
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.eth.Reactor().Post("messages", state.Manifest().Messages)
|
sm.eth.EventMux().Post(state.Manifest().Messages)
|
||||||
|
|
||||||
return bloomf
|
return bloomf
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ type TxMsgTy byte
|
|||||||
const (
|
const (
|
||||||
TxPre = iota
|
TxPre = iota
|
||||||
TxPost
|
TxPost
|
||||||
|
|
||||||
minGasPrice = 1000000
|
minGasPrice = 1000000
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -160,7 +161,7 @@ out:
|
|||||||
txplogger.Debugf("(t) %x => %x (%v) %x\n", tx.Sender()[:4], tmp, tx.Value, tx.Hash())
|
txplogger.Debugf("(t) %x => %x (%v) %x\n", tx.Sender()[:4], tmp, tx.Value, tx.Hash())
|
||||||
|
|
||||||
// Notify the subscribers
|
// Notify the subscribers
|
||||||
pool.Ethereum.Reactor().Post("newTx:pre", tx)
|
pool.Ethereum.EventMux().Post(TxEvent{TxPre, tx})
|
||||||
}
|
}
|
||||||
case <-pool.quit:
|
case <-pool.quit:
|
||||||
break out
|
break out
|
||||||
|
53
ethereum.go
53
ethereum.go
@ -17,12 +17,11 @@ import (
|
|||||||
"github.com/ethereum/eth-go/ethchain"
|
"github.com/ethereum/eth-go/ethchain"
|
||||||
"github.com/ethereum/eth-go/ethcrypto"
|
"github.com/ethereum/eth-go/ethcrypto"
|
||||||
"github.com/ethereum/eth-go/ethlog"
|
"github.com/ethereum/eth-go/ethlog"
|
||||||
"github.com/ethereum/eth-go/ethreact"
|
|
||||||
"github.com/ethereum/eth-go/ethrpc"
|
"github.com/ethereum/eth-go/ethrpc"
|
||||||
"github.com/ethereum/eth-go/ethstate"
|
"github.com/ethereum/eth-go/ethstate"
|
||||||
"github.com/ethereum/eth-go/ethutil"
|
"github.com/ethereum/eth-go/ethutil"
|
||||||
"github.com/ethereum/eth-go/ethwire"
|
"github.com/ethereum/eth-go/ethwire"
|
||||||
"github.com/ethereum/eth-go/eventer"
|
"github.com/ethereum/eth-go/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -60,7 +59,7 @@ type Ethereum struct {
|
|||||||
// The block pool
|
// The block pool
|
||||||
blockPool *BlockPool
|
blockPool *BlockPool
|
||||||
// Eventer
|
// Eventer
|
||||||
eventer *eventer.EventMachine
|
eventMux event.TypeMux
|
||||||
// Peers
|
// Peers
|
||||||
peers *list.List
|
peers *list.List
|
||||||
// Nonce
|
// Nonce
|
||||||
@ -85,8 +84,6 @@ type Ethereum struct {
|
|||||||
|
|
||||||
listening bool
|
listening bool
|
||||||
|
|
||||||
reactor *ethreact.ReactorEngine
|
|
||||||
|
|
||||||
RpcServer *ethrpc.JsonRpcServer
|
RpcServer *ethrpc.JsonRpcServer
|
||||||
|
|
||||||
keyManager *ethcrypto.KeyManager
|
keyManager *ethcrypto.KeyManager
|
||||||
@ -129,8 +126,6 @@ func New(db ethutil.Database, clientIdentity ethwire.ClientIdentity, keyManager
|
|||||||
isUpToDate: true,
|
isUpToDate: true,
|
||||||
filters: make(map[int]*ethchain.Filter),
|
filters: make(map[int]*ethchain.Filter),
|
||||||
}
|
}
|
||||||
ethereum.reactor = ethreact.New()
|
|
||||||
ethereum.eventer = eventer.New()
|
|
||||||
|
|
||||||
ethereum.blockPool = NewBlockPool(ethereum)
|
ethereum.blockPool = NewBlockPool(ethereum)
|
||||||
ethereum.txPool = ethchain.NewTxPool(ethereum)
|
ethereum.txPool = ethchain.NewTxPool(ethereum)
|
||||||
@ -143,10 +138,6 @@ func New(db ethutil.Database, clientIdentity ethwire.ClientIdentity, keyManager
|
|||||||
return ethereum, nil
|
return ethereum, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Ethereum) Reactor() *ethreact.ReactorEngine {
|
|
||||||
return s.reactor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Ethereum) KeyManager() *ethcrypto.KeyManager {
|
func (s *Ethereum) KeyManager() *ethcrypto.KeyManager {
|
||||||
return s.keyManager
|
return s.keyManager
|
||||||
}
|
}
|
||||||
@ -169,8 +160,8 @@ func (s *Ethereum) TxPool() *ethchain.TxPool {
|
|||||||
func (s *Ethereum) BlockPool() *BlockPool {
|
func (s *Ethereum) BlockPool() *BlockPool {
|
||||||
return s.blockPool
|
return s.blockPool
|
||||||
}
|
}
|
||||||
func (s *Ethereum) Eventer() *eventer.EventMachine {
|
func (s *Ethereum) EventMux() *event.TypeMux {
|
||||||
return s.eventer
|
return &s.eventMux
|
||||||
}
|
}
|
||||||
func (self *Ethereum) Db() ethutil.Database {
|
func (self *Ethereum) Db() ethutil.Database {
|
||||||
return self.db
|
return self.db
|
||||||
@ -376,7 +367,7 @@ func (s *Ethereum) removePeerElement(e *list.Element) {
|
|||||||
|
|
||||||
s.peers.Remove(e)
|
s.peers.Remove(e)
|
||||||
|
|
||||||
s.reactor.Post("peerList", s.peers)
|
s.eventMux.Post(PeerListEvent{s.peers})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Ethereum) RemovePeer(p *Peer) {
|
func (s *Ethereum) RemovePeer(p *Peer) {
|
||||||
@ -400,7 +391,6 @@ func (s *Ethereum) reapDeadPeerHandler() {
|
|||||||
|
|
||||||
// Start the ethereum
|
// Start the ethereum
|
||||||
func (s *Ethereum) Start(seed bool) {
|
func (s *Ethereum) Start(seed bool) {
|
||||||
s.reactor.Start()
|
|
||||||
s.blockPool.Start()
|
s.blockPool.Start()
|
||||||
s.stateManager.Start()
|
s.stateManager.Start()
|
||||||
|
|
||||||
@ -524,8 +514,7 @@ func (s *Ethereum) Stop() {
|
|||||||
}
|
}
|
||||||
s.txPool.Stop()
|
s.txPool.Stop()
|
||||||
s.stateManager.Stop()
|
s.stateManager.Stop()
|
||||||
s.reactor.Flush()
|
s.eventMux.Stop()
|
||||||
s.reactor.Stop()
|
|
||||||
s.blockPool.Stop()
|
s.blockPool.Stop()
|
||||||
|
|
||||||
ethlogger.Infoln("Server stopped")
|
ethlogger.Infoln("Server stopped")
|
||||||
@ -584,10 +573,10 @@ out:
|
|||||||
select {
|
select {
|
||||||
case <-upToDateTimer.C:
|
case <-upToDateTimer.C:
|
||||||
if self.IsUpToDate() && !self.isUpToDate {
|
if self.IsUpToDate() && !self.isUpToDate {
|
||||||
self.reactor.Post("chainSync", false)
|
self.eventMux.Post(ChainSyncEvent{false})
|
||||||
self.isUpToDate = true
|
self.isUpToDate = true
|
||||||
} else if !self.IsUpToDate() && self.isUpToDate {
|
} else if !self.IsUpToDate() && self.isUpToDate {
|
||||||
self.reactor.Post("chainSync", true)
|
self.eventMux.Post(ChainSyncEvent{true})
|
||||||
self.isUpToDate = false
|
self.isUpToDate = false
|
||||||
}
|
}
|
||||||
case <-self.quit:
|
case <-self.quit:
|
||||||
@ -623,33 +612,24 @@ func (self *Ethereum) GetFilter(id int) *ethchain.Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *Ethereum) filterLoop() {
|
func (self *Ethereum) filterLoop() {
|
||||||
blockChan := make(chan ethreact.Event, 5)
|
|
||||||
messageChan := make(chan ethreact.Event, 5)
|
|
||||||
// Subscribe to events
|
// Subscribe to events
|
||||||
reactor := self.Reactor()
|
events := self.eventMux.Subscribe(ethchain.NewBlockEvent{}, ethstate.Messages(nil))
|
||||||
reactor.Subscribe("newBlock", blockChan)
|
for event := range events.Chan() {
|
||||||
reactor.Subscribe("messages", messageChan)
|
switch event := event.(type) {
|
||||||
out:
|
case ethchain.NewBlockEvent:
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-self.quit:
|
|
||||||
break out
|
|
||||||
case block := <-blockChan:
|
|
||||||
if block, ok := block.Resource.(*ethchain.Block); ok {
|
|
||||||
self.filterMu.RLock()
|
self.filterMu.RLock()
|
||||||
for _, filter := range self.filters {
|
for _, filter := range self.filters {
|
||||||
if filter.BlockCallback != nil {
|
if filter.BlockCallback != nil {
|
||||||
filter.BlockCallback(block)
|
filter.BlockCallback(event.Block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.filterMu.RUnlock()
|
self.filterMu.RUnlock()
|
||||||
}
|
|
||||||
case msg := <-messageChan:
|
case ethstate.Messages:
|
||||||
if messages, ok := msg.Resource.(ethstate.Messages); ok {
|
|
||||||
self.filterMu.RLock()
|
self.filterMu.RLock()
|
||||||
for _, filter := range self.filters {
|
for _, filter := range self.filters {
|
||||||
if filter.MessageCallback != nil {
|
if filter.MessageCallback != nil {
|
||||||
msgs := filter.FilterMessages(messages)
|
msgs := filter.FilterMessages(event)
|
||||||
if len(msgs) > 0 {
|
if len(msgs) > 0 {
|
||||||
filter.MessageCallback(msgs)
|
filter.MessageCallback(msgs)
|
||||||
}
|
}
|
||||||
@ -659,7 +639,6 @@ out:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func bootstrapDb(db ethutil.Database) {
|
func bootstrapDb(db ethutil.Database) {
|
||||||
d, _ := db.Get([]byte("ProtocolVersion"))
|
d, _ := db.Get([]byte("ProtocolVersion"))
|
||||||
|
@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/ethereum/eth-go/ethchain"
|
"github.com/ethereum/eth-go/ethchain"
|
||||||
"github.com/ethereum/eth-go/ethlog"
|
"github.com/ethereum/eth-go/ethlog"
|
||||||
"github.com/ethereum/eth-go/ethreact"
|
|
||||||
"github.com/ethereum/eth-go/ethwire"
|
"github.com/ethereum/eth-go/ethwire"
|
||||||
|
"github.com/ethereum/eth-go/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger = ethlog.NewLogger("MINER")
|
var logger = ethlog.NewLogger("MINER")
|
||||||
@ -16,17 +16,27 @@ type Miner struct {
|
|||||||
pow ethchain.PoW
|
pow ethchain.PoW
|
||||||
ethereum ethchain.EthManager
|
ethereum ethchain.EthManager
|
||||||
coinbase []byte
|
coinbase []byte
|
||||||
reactChan chan ethreact.Event
|
|
||||||
txs ethchain.Transactions
|
txs ethchain.Transactions
|
||||||
uncles []*ethchain.Block
|
uncles []*ethchain.Block
|
||||||
block *ethchain.Block
|
block *ethchain.Block
|
||||||
powChan chan []byte
|
|
||||||
powQuitChan chan ethreact.Event
|
events event.Subscription
|
||||||
quitChan chan chan error
|
powQuitChan chan struct{}
|
||||||
|
powDone chan struct{}
|
||||||
|
|
||||||
turbo bool
|
turbo bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
Started = iota
|
||||||
|
Stopped
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Type int // Started || Stopped
|
||||||
|
Miner *Miner
|
||||||
|
}
|
||||||
|
|
||||||
func (self *Miner) GetPow() ethchain.PoW {
|
func (self *Miner) GetPow() ethchain.PoW {
|
||||||
return self.pow
|
return self.pow
|
||||||
}
|
}
|
||||||
@ -48,46 +58,42 @@ func (self *Miner) ToggleTurbo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (miner *Miner) Start() {
|
func (miner *Miner) Start() {
|
||||||
miner.reactChan = make(chan ethreact.Event, 1) // This is the channel that receives 'updates' when ever a new transaction or block comes in
|
|
||||||
miner.powChan = make(chan []byte, 1) // This is the channel that receives valid sha hashes for a given block
|
|
||||||
miner.powQuitChan = make(chan ethreact.Event, 1) // This is the channel that can exit the miner thread
|
|
||||||
miner.quitChan = make(chan chan error, 1)
|
|
||||||
|
|
||||||
// Insert initial TXs in our little miner 'pool'
|
// Insert initial TXs in our little miner 'pool'
|
||||||
miner.txs = miner.ethereum.TxPool().Flush()
|
miner.txs = miner.ethereum.TxPool().Flush()
|
||||||
miner.block = miner.ethereum.BlockChain().NewBlock(miner.coinbase)
|
miner.block = miner.ethereum.BlockChain().NewBlock(miner.coinbase)
|
||||||
|
|
||||||
|
mux := miner.ethereum.EventMux()
|
||||||
|
miner.events = mux.Subscribe(ethchain.NewBlockEvent{}, ethchain.TxEvent{})
|
||||||
|
|
||||||
// Prepare inital block
|
// Prepare inital block
|
||||||
//miner.ethereum.StateManager().Prepare(miner.block.State(), miner.block.State())
|
//miner.ethereum.StateManager().Prepare(miner.block.State(), miner.block.State())
|
||||||
go miner.listener()
|
go miner.listener()
|
||||||
|
|
||||||
reactor := miner.ethereum.Reactor()
|
|
||||||
reactor.Subscribe("newBlock", miner.reactChan)
|
|
||||||
reactor.Subscribe("newTx:pre", miner.reactChan)
|
|
||||||
|
|
||||||
// We need the quit chan to be a Reactor event.
|
|
||||||
// The POW search method is actually blocking and if we don't
|
|
||||||
// listen to the reactor events inside of the pow itself
|
|
||||||
// The miner overseer will never get the reactor events themselves
|
|
||||||
// Only after the miner will find the sha
|
|
||||||
reactor.Subscribe("newBlock", miner.powQuitChan)
|
|
||||||
reactor.Subscribe("newTx:pre", miner.powQuitChan)
|
|
||||||
|
|
||||||
logger.Infoln("Started")
|
logger.Infoln("Started")
|
||||||
|
mux.Post(Event{Started, miner})
|
||||||
|
}
|
||||||
|
|
||||||
reactor.Post("miner:start", miner)
|
func (miner *Miner) Stop() {
|
||||||
|
logger.Infoln("Stopping...")
|
||||||
|
miner.events.Unsubscribe()
|
||||||
|
miner.ethereum.EventMux().Post(Event{Stopped, miner})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (miner *Miner) listener() {
|
func (miner *Miner) listener() {
|
||||||
for {
|
for {
|
||||||
select {
|
miner.startMining()
|
||||||
case status := <-miner.quitChan:
|
|
||||||
logger.Infoln("Stopped")
|
|
||||||
status <- nil
|
|
||||||
return
|
|
||||||
case chanMessage := <-miner.reactChan:
|
|
||||||
|
|
||||||
if block, ok := chanMessage.Resource.(*ethchain.Block); ok {
|
select {
|
||||||
|
case event, isopen := <-miner.events.Chan():
|
||||||
|
miner.stopMining()
|
||||||
|
if !isopen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch event := event.(type) {
|
||||||
|
case ethchain.NewBlockEvent:
|
||||||
|
block := event.Block
|
||||||
//logger.Infoln("Got new block via Reactor")
|
//logger.Infoln("Got new block via Reactor")
|
||||||
if bytes.Compare(miner.ethereum.BlockChain().CurrentBlock.Hash(), block.Hash()) == 0 {
|
if bytes.Compare(miner.ethereum.BlockChain().CurrentBlock.Hash(), block.Hash()) == 0 {
|
||||||
// TODO: Perhaps continue mining to get some uncle rewards
|
// TODO: Perhaps continue mining to get some uncle rewards
|
||||||
@ -117,49 +123,44 @@ func (miner *Miner) listener() {
|
|||||||
miner.uncles = append(miner.uncles, block)
|
miner.uncles = append(miner.uncles, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tx, ok := chanMessage.Resource.(*ethchain.Transaction); ok {
|
case ethchain.TxEvent:
|
||||||
|
if event.Type == ethchain.TxPre {
|
||||||
found := false
|
found := false
|
||||||
for _, ctx := range miner.txs {
|
for _, ctx := range miner.txs {
|
||||||
if found = bytes.Compare(ctx.Hash(), tx.Hash()) == 0; found {
|
if found = bytes.Compare(ctx.Hash(), event.Tx.Hash()) == 0; found {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if found == false {
|
if found == false {
|
||||||
// Undo all previous commits
|
// Undo all previous commits
|
||||||
miner.block.Undo()
|
miner.block.Undo()
|
||||||
// Apply new transactions
|
// Apply new transactions
|
||||||
miner.txs = append(miner.txs, tx)
|
miner.txs = append(miner.txs, event.Tx)
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
miner.mineNewBlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (miner *Miner) Stop() {
|
case <-miner.powDone:
|
||||||
logger.Infoln("Stopping...")
|
// next iteration will start mining again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
miner.powQuitChan <- ethreact.Event{}
|
func (miner *Miner) startMining() {
|
||||||
|
if miner.powDone == nil {
|
||||||
|
miner.powDone = make(chan struct{})
|
||||||
|
}
|
||||||
|
miner.powQuitChan = make(chan struct{})
|
||||||
|
go miner.mineNewBlock()
|
||||||
|
}
|
||||||
|
|
||||||
status := make(chan error)
|
func (miner *Miner) stopMining() {
|
||||||
miner.quitChan <- status
|
close(miner.powQuitChan)
|
||||||
<-status
|
<-miner.powDone
|
||||||
|
|
||||||
reactor := miner.ethereum.Reactor()
|
|
||||||
reactor.Unsubscribe("newBlock", miner.powQuitChan)
|
|
||||||
reactor.Unsubscribe("newTx:pre", miner.powQuitChan)
|
|
||||||
reactor.Unsubscribe("newBlock", miner.reactChan)
|
|
||||||
reactor.Unsubscribe("newTx:pre", miner.reactChan)
|
|
||||||
|
|
||||||
reactor.Post("miner:stop", miner)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Miner) mineNewBlock() {
|
func (self *Miner) mineNewBlock() {
|
||||||
|
|
||||||
stateManager := self.ethereum.StateManager()
|
stateManager := self.ethereum.StateManager()
|
||||||
|
|
||||||
self.block = self.ethereum.BlockChain().NewBlock(self.coinbase)
|
self.block = self.ethereum.BlockChain().NewBlock(self.coinbase)
|
||||||
@ -195,8 +196,9 @@ func (self *Miner) mineNewBlock() {
|
|||||||
logger.Infof("Mining on block. Includes %v transactions", len(self.txs))
|
logger.Infof("Mining on block. Includes %v transactions", len(self.txs))
|
||||||
|
|
||||||
// Find a valid nonce
|
// Find a valid nonce
|
||||||
self.block.Nonce = self.pow.Search(self.block, self.powQuitChan)
|
nonce := self.pow.Search(self.block, self.powQuitChan)
|
||||||
if self.block.Nonce != nil {
|
if nonce != nil {
|
||||||
|
self.block.Nonce = nonce
|
||||||
err := self.ethereum.StateManager().Process(self.block, false)
|
err := self.ethereum.StateManager().Process(self.block, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infoln(err)
|
logger.Infoln(err)
|
||||||
@ -208,4 +210,5 @@ func (self *Miner) mineNewBlock() {
|
|||||||
self.txs = self.ethereum.TxPool().CurrentTransactions()
|
self.txs = self.ethereum.TxPool().CurrentTransactions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.powDone <- struct{}{}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
## Reactor
|
|
||||||
|
|
||||||
Reactor is the internal broadcast engine that allows components to be notified of ethereum stack events such as finding new blocks or change in state.
|
|
||||||
Event notification is handled via subscription:
|
|
||||||
|
|
||||||
var blockChan = make(chan ethreact.Event, 10)
|
|
||||||
reactor.Subscribe("newBlock", blockChan)
|
|
||||||
|
|
||||||
ethreact.Event broadcast on the channel are
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
Resource interface{}
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
Resource is polimorphic depending on the event type and should be typecast before use, e.g:
|
|
||||||
|
|
||||||
b := <-blockChan:
|
|
||||||
block := b.Resource.(*ethchain.Block)
|
|
||||||
|
|
||||||
Events are guaranteed to be broadcast in order but the broadcast never blocks or leaks which means while the subscribing event channel is blocked (e.g., full if buffered) further messages will be skipped.
|
|
||||||
|
|
||||||
The engine allows arbitrary events to be posted and subscribed to.
|
|
||||||
|
|
||||||
ethereum.Reactor().Post("newBlock", newBlock)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
|||||||
package ethreact
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/eth-go/ethlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger = ethlog.NewLogger("REACTOR")
|
|
||||||
|
|
||||||
const (
|
|
||||||
eventBufferSize int = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
type EventHandler struct {
|
|
||||||
lock sync.RWMutex
|
|
||||||
name string
|
|
||||||
chans []chan Event
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post the Event with the reactor resource on the channels
|
|
||||||
// currently subscribed to the event
|
|
||||||
func (e *EventHandler) Post(event Event) {
|
|
||||||
e.lock.RLock()
|
|
||||||
defer e.lock.RUnlock()
|
|
||||||
|
|
||||||
// if we want to preserve order pushing to subscibed channels
|
|
||||||
// dispatching should be syncrounous
|
|
||||||
// this means if subscribed event channel is blocked
|
|
||||||
// the reactor dispatch will be blocked, so we need to mitigate by skipping
|
|
||||||
// rogue blocking subscribers
|
|
||||||
for i, ch := range e.chans {
|
|
||||||
select {
|
|
||||||
case ch <- event:
|
|
||||||
default:
|
|
||||||
logger.Debugf("subscribing channel %d to event %s blocked. skipping\n", i, event.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a subscriber to this event
|
|
||||||
func (e *EventHandler) Add(ch chan Event) {
|
|
||||||
e.lock.Lock()
|
|
||||||
defer e.lock.Unlock()
|
|
||||||
|
|
||||||
e.chans = append(e.chans, ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a subscriber
|
|
||||||
func (e *EventHandler) Remove(ch chan Event) int {
|
|
||||||
e.lock.Lock()
|
|
||||||
defer e.lock.Unlock()
|
|
||||||
|
|
||||||
for i, c := range e.chans {
|
|
||||||
if c == ch {
|
|
||||||
e.chans = append(e.chans[:i], e.chans[i+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(e.chans)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic reactor event
|
|
||||||
type Event struct {
|
|
||||||
Resource interface{}
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// The reactor basic engine. Acts as bridge
|
|
||||||
// between the events and the subscribers/posters
|
|
||||||
type ReactorEngine struct {
|
|
||||||
lock sync.RWMutex
|
|
||||||
eventChannel chan Event
|
|
||||||
eventHandlers map[string]*EventHandler
|
|
||||||
quit chan chan error
|
|
||||||
running bool
|
|
||||||
drained chan bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *ReactorEngine {
|
|
||||||
return &ReactorEngine{
|
|
||||||
eventHandlers: make(map[string]*EventHandler),
|
|
||||||
eventChannel: make(chan Event, eventBufferSize),
|
|
||||||
quit: make(chan chan error, 1),
|
|
||||||
drained: make(chan bool, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reactor *ReactorEngine) Start() {
|
|
||||||
reactor.lock.Lock()
|
|
||||||
defer reactor.lock.Unlock()
|
|
||||||
if !reactor.running {
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case status := <-reactor.quit:
|
|
||||||
reactor.lock.Lock()
|
|
||||||
defer reactor.lock.Unlock()
|
|
||||||
reactor.running = false
|
|
||||||
logger.Infoln("stopped")
|
|
||||||
status <- nil
|
|
||||||
return
|
|
||||||
case event := <-reactor.eventChannel:
|
|
||||||
// needs to be called syncronously to keep order of events
|
|
||||||
reactor.dispatch(event)
|
|
||||||
default:
|
|
||||||
reactor.drained <- true // blocking till message is coming in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
reactor.running = true
|
|
||||||
logger.Infoln("started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reactor *ReactorEngine) Stop() {
|
|
||||||
if reactor.running {
|
|
||||||
status := make(chan error)
|
|
||||||
reactor.quit <- status
|
|
||||||
select {
|
|
||||||
case <-reactor.drained:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
<-status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reactor *ReactorEngine) Flush() {
|
|
||||||
<-reactor.drained
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe a channel to the specified event
|
|
||||||
func (reactor *ReactorEngine) Subscribe(event string, eventChannel chan Event) {
|
|
||||||
reactor.lock.Lock()
|
|
||||||
defer reactor.lock.Unlock()
|
|
||||||
|
|
||||||
eventHandler := reactor.eventHandlers[event]
|
|
||||||
// Create a new event handler if one isn't available
|
|
||||||
if eventHandler == nil {
|
|
||||||
eventHandler = &EventHandler{name: event}
|
|
||||||
reactor.eventHandlers[event] = eventHandler
|
|
||||||
}
|
|
||||||
// Add the events channel to reactor event handler
|
|
||||||
eventHandler.Add(eventChannel)
|
|
||||||
logger.Debugf("added new subscription to %s", event)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reactor *ReactorEngine) Unsubscribe(event string, eventChannel chan Event) {
|
|
||||||
reactor.lock.Lock()
|
|
||||||
defer reactor.lock.Unlock()
|
|
||||||
|
|
||||||
eventHandler := reactor.eventHandlers[event]
|
|
||||||
if eventHandler != nil {
|
|
||||||
len := eventHandler.Remove(eventChannel)
|
|
||||||
if len == 0 {
|
|
||||||
reactor.eventHandlers[event] = nil
|
|
||||||
}
|
|
||||||
logger.Debugf("removed subscription to %s", event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reactor *ReactorEngine) Post(event string, resource interface{}) {
|
|
||||||
reactor.lock.Lock()
|
|
||||||
defer reactor.lock.Unlock()
|
|
||||||
|
|
||||||
if reactor.running {
|
|
||||||
reactor.eventChannel <- Event{Resource: resource, Name: event}
|
|
||||||
select {
|
|
||||||
case <-reactor.drained:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reactor *ReactorEngine) dispatch(event Event) {
|
|
||||||
name := event.Name
|
|
||||||
eventHandler := reactor.eventHandlers[name]
|
|
||||||
// if no subscriptions to this event type - no event handler created
|
|
||||||
// then noone to notify
|
|
||||||
if eventHandler != nil {
|
|
||||||
// needs to be called syncronously
|
|
||||||
eventHandler.Post(event)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package ethreact
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReactorAdd(t *testing.T) {
|
|
||||||
reactor := New()
|
|
||||||
ch := make(chan Event)
|
|
||||||
reactor.Subscribe("test", ch)
|
|
||||||
if reactor.eventHandlers["test"] == nil {
|
|
||||||
t.Error("Expected new eventHandler to be created")
|
|
||||||
}
|
|
||||||
reactor.Unsubscribe("test", ch)
|
|
||||||
if reactor.eventHandlers["test"] != nil {
|
|
||||||
t.Error("Expected eventHandler to be removed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReactorEvent(t *testing.T) {
|
|
||||||
var name string
|
|
||||||
reactor := New()
|
|
||||||
// Buffer the channel, so it doesn't block for this test
|
|
||||||
cap := 20
|
|
||||||
ch := make(chan Event, cap)
|
|
||||||
reactor.Subscribe("even", ch)
|
|
||||||
reactor.Subscribe("odd", ch)
|
|
||||||
reactor.Post("even", "disappears") // should not broadcast if engine not started
|
|
||||||
reactor.Start()
|
|
||||||
for i := 0; i < cap; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
name = "even"
|
|
||||||
} else {
|
|
||||||
name = "odd"
|
|
||||||
}
|
|
||||||
reactor.Post(name, i)
|
|
||||||
}
|
|
||||||
reactor.Post("test", cap) // this should not block
|
|
||||||
i := 0
|
|
||||||
reactor.Flush()
|
|
||||||
close(ch)
|
|
||||||
for event := range ch {
|
|
||||||
fmt.Printf("%d: %v", i, event)
|
|
||||||
if i%2 == 0 {
|
|
||||||
name = "even"
|
|
||||||
} else {
|
|
||||||
name = "odd"
|
|
||||||
}
|
|
||||||
if val, ok := event.Resource.(int); ok {
|
|
||||||
if i != val || event.Name != name {
|
|
||||||
t.Error("Expected event %d to be of type %s and resource %d, got ", i, name, i, val)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Error("Unable to cast")
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if i != cap {
|
|
||||||
t.Error("excpected exactly %d events, got ", i)
|
|
||||||
}
|
|
||||||
reactor.Stop()
|
|
||||||
}
|
|
183
event/event.go
Normal file
183
event/event.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
// Package event implements an event multiplexer.
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subscription is implemented by event subscriptions.
|
||||||
|
type Subscription interface {
|
||||||
|
// Chan returns a channel that carries events.
|
||||||
|
// Implementations should return the same channel
|
||||||
|
// for any subsequent calls to Chan.
|
||||||
|
Chan() <-chan interface{}
|
||||||
|
|
||||||
|
// Unsubscribe stops delivery of events to a subscription.
|
||||||
|
// The event channel is closed.
|
||||||
|
// Unsubscribe can be called more than once.
|
||||||
|
Unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TypeMux dispatches events to registered receivers. Receivers can be
|
||||||
|
// registered to handle events of certain type. Any operation
|
||||||
|
// called after mux is stopped will return ErrMuxClosed.
|
||||||
|
//
|
||||||
|
// The zero value is ready to use.
|
||||||
|
type TypeMux struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
subm map[reflect.Type][]*muxsub
|
||||||
|
stopped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrMuxClosed is returned when Posting on a closed TypeMux.
|
||||||
|
var ErrMuxClosed = errors.New("event: mux closed")
|
||||||
|
|
||||||
|
// Subscribe creates a subscription for events of the given types. The
|
||||||
|
// subscription's channel is closed when it is unsubscribed
|
||||||
|
// or the mux is closed.
|
||||||
|
func (mux *TypeMux) Subscribe(types ...interface{}) Subscription {
|
||||||
|
sub := newsub(mux)
|
||||||
|
mux.mutex.Lock()
|
||||||
|
defer mux.mutex.Unlock()
|
||||||
|
if mux.stopped {
|
||||||
|
close(sub.postC)
|
||||||
|
} else {
|
||||||
|
if mux.subm == nil {
|
||||||
|
mux.subm = make(map[reflect.Type][]*muxsub)
|
||||||
|
}
|
||||||
|
for _, t := range types {
|
||||||
|
rtyp := reflect.TypeOf(t)
|
||||||
|
oldsubs := mux.subm[rtyp]
|
||||||
|
if find(oldsubs, sub) != -1 {
|
||||||
|
panic(fmt.Sprintf("event: duplicate type %s in Subscribe", rtyp))
|
||||||
|
}
|
||||||
|
subs := make([]*muxsub, len(oldsubs)+1)
|
||||||
|
copy(subs, oldsubs)
|
||||||
|
subs[len(oldsubs)] = sub
|
||||||
|
mux.subm[rtyp] = subs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post sends an event to all receivers registered for the given type.
|
||||||
|
// It returns ErrMuxClosed if the mux has been stopped.
|
||||||
|
func (mux *TypeMux) Post(ev interface{}) error {
|
||||||
|
rtyp := reflect.TypeOf(ev)
|
||||||
|
mux.mutex.RLock()
|
||||||
|
if mux.stopped {
|
||||||
|
mux.mutex.RUnlock()
|
||||||
|
return ErrMuxClosed
|
||||||
|
}
|
||||||
|
subs := mux.subm[rtyp]
|
||||||
|
mux.mutex.RUnlock()
|
||||||
|
for _, sub := range subs {
|
||||||
|
sub.deliver(ev)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop closes a mux. The mux can no longer be used.
|
||||||
|
// Future Post calls will fail with ErrMuxClosed.
|
||||||
|
// Stop blocks until all current deliveries have finished.
|
||||||
|
func (mux *TypeMux) Stop() {
|
||||||
|
mux.mutex.Lock()
|
||||||
|
for _, subs := range mux.subm {
|
||||||
|
for _, sub := range subs {
|
||||||
|
sub.closewait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mux.subm = nil
|
||||||
|
mux.stopped = true
|
||||||
|
mux.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *TypeMux) del(s *muxsub) {
|
||||||
|
mux.mutex.Lock()
|
||||||
|
for typ, subs := range mux.subm {
|
||||||
|
if pos := find(subs, s); pos >= 0 {
|
||||||
|
if len(subs) == 1 {
|
||||||
|
delete(mux.subm, typ)
|
||||||
|
} else {
|
||||||
|
mux.subm[typ] = posdelete(subs, pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mux.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func find(slice []*muxsub, item *muxsub) int {
|
||||||
|
for i, v := range slice {
|
||||||
|
if v == item {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func posdelete(slice []*muxsub, pos int) []*muxsub {
|
||||||
|
news := make([]*muxsub, len(slice)-1)
|
||||||
|
copy(news[:pos], slice[:pos])
|
||||||
|
copy(news[pos:], slice[pos+1:])
|
||||||
|
return news
|
||||||
|
}
|
||||||
|
|
||||||
|
type muxsub struct {
|
||||||
|
mux *TypeMux
|
||||||
|
closeMu sync.Mutex
|
||||||
|
closing chan struct{}
|
||||||
|
closed bool
|
||||||
|
|
||||||
|
// these two are the same channel. they are stored separately so
|
||||||
|
// postC can be set to nil without affecting the return value of
|
||||||
|
// Chan.
|
||||||
|
postMu sync.RWMutex
|
||||||
|
readC <-chan interface{}
|
||||||
|
postC chan<- interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newsub(mux *TypeMux) *muxsub {
|
||||||
|
c := make(chan interface{})
|
||||||
|
return &muxsub{
|
||||||
|
mux: mux,
|
||||||
|
readC: c,
|
||||||
|
postC: c,
|
||||||
|
closing: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *muxsub) Chan() <-chan interface{} {
|
||||||
|
return s.readC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *muxsub) Unsubscribe() {
|
||||||
|
s.mux.del(s)
|
||||||
|
s.closewait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *muxsub) closewait() {
|
||||||
|
s.closeMu.Lock()
|
||||||
|
defer s.closeMu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(s.closing)
|
||||||
|
s.closed = true
|
||||||
|
|
||||||
|
s.postMu.Lock()
|
||||||
|
close(s.postC)
|
||||||
|
s.postC = nil
|
||||||
|
s.postMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *muxsub) deliver(ev interface{}) {
|
||||||
|
s.postMu.RLock()
|
||||||
|
select {
|
||||||
|
case s.postC <- ev:
|
||||||
|
case <-s.closing:
|
||||||
|
}
|
||||||
|
s.postMu.RUnlock()
|
||||||
|
}
|
176
event/event_test.go
Normal file
176
event/event_test.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testEvent int
|
||||||
|
|
||||||
|
func TestSub(t *testing.T) {
|
||||||
|
mux := new(TypeMux)
|
||||||
|
defer mux.Stop()
|
||||||
|
|
||||||
|
sub := mux.Subscribe(testEvent(0))
|
||||||
|
go func() {
|
||||||
|
if err := mux.Post(testEvent(5)); err != nil {
|
||||||
|
t.Errorf("Post returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ev := <-sub.Chan()
|
||||||
|
|
||||||
|
if ev.(testEvent) != testEvent(5) {
|
||||||
|
t.Errorf("Got %v (%T), expected event %v (%T)",
|
||||||
|
ev, ev, testEvent(5), testEvent(5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxErrorAfterStop(t *testing.T) {
|
||||||
|
mux := new(TypeMux)
|
||||||
|
mux.Stop()
|
||||||
|
|
||||||
|
sub := mux.Subscribe(testEvent(0))
|
||||||
|
if _, isopen := <-sub.Chan(); isopen {
|
||||||
|
t.Errorf("subscription channel was not closed")
|
||||||
|
}
|
||||||
|
if err := mux.Post(testEvent(0)); err != ErrMuxClosed {
|
||||||
|
t.Errorf("Post error mismatch, got: %s, expected: %s", err, ErrMuxClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnsubscribeUnblockPost(t *testing.T) {
|
||||||
|
mux := new(TypeMux)
|
||||||
|
defer mux.Stop()
|
||||||
|
|
||||||
|
sub := mux.Subscribe(testEvent(0))
|
||||||
|
unblocked := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
mux.Post(testEvent(5))
|
||||||
|
unblocked <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-unblocked:
|
||||||
|
t.Errorf("Post returned before Unsubscribe")
|
||||||
|
default:
|
||||||
|
sub.Unsubscribe()
|
||||||
|
<-unblocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubscribeDuplicateType(t *testing.T) {
|
||||||
|
mux := new(TypeMux)
|
||||||
|
expected := "event: duplicate type event.testEvent in Subscribe"
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := recover()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Subscribe didn't panic for duplicate type")
|
||||||
|
} else if err != expected {
|
||||||
|
t.Errorf("panic mismatch: got %#v, expected %#v", err, expected)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
mux.Subscribe(testEvent(1), testEvent(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxConcurrent(t *testing.T) {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
mux := new(TypeMux)
|
||||||
|
defer mux.Stop()
|
||||||
|
|
||||||
|
recv := make(chan int)
|
||||||
|
poster := func() {
|
||||||
|
for {
|
||||||
|
err := mux.Post(testEvent(0))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sub := func(i int) {
|
||||||
|
time.Sleep(time.Duration(rand.Intn(99)) * time.Millisecond)
|
||||||
|
sub := mux.Subscribe(testEvent(0))
|
||||||
|
<-sub.Chan()
|
||||||
|
sub.Unsubscribe()
|
||||||
|
recv <- i
|
||||||
|
}
|
||||||
|
|
||||||
|
go poster()
|
||||||
|
go poster()
|
||||||
|
go poster()
|
||||||
|
nsubs := 1000
|
||||||
|
for i := 0; i < nsubs; i++ {
|
||||||
|
go sub(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until everyone has been served
|
||||||
|
counts := make(map[int]int, nsubs)
|
||||||
|
for i := 0; i < nsubs; i++ {
|
||||||
|
counts[<-recv]++
|
||||||
|
}
|
||||||
|
for i, count := range counts {
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("receiver %d called %d times, expected only 1 call", i, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptySubscriber(mux *TypeMux, types ...interface{}) {
|
||||||
|
s := mux.Subscribe(testEvent(0))
|
||||||
|
go func() {
|
||||||
|
for _ = range s.Chan() {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPost3(b *testing.B) {
|
||||||
|
var mux = new(TypeMux)
|
||||||
|
defer mux.Stop()
|
||||||
|
emptySubscriber(mux, testEvent(0))
|
||||||
|
emptySubscriber(mux, testEvent(0))
|
||||||
|
emptySubscriber(mux, testEvent(0))
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
mux.Post(testEvent(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPostConcurrent(b *testing.B) {
|
||||||
|
var mux = new(TypeMux)
|
||||||
|
defer mux.Stop()
|
||||||
|
emptySubscriber(mux, testEvent(0))
|
||||||
|
emptySubscriber(mux, testEvent(0))
|
||||||
|
emptySubscriber(mux, testEvent(0))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
poster := func() {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
mux.Post(testEvent(0))
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
wg.Add(5)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
go poster()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// for comparison
|
||||||
|
func BenchmarkChanSend(b *testing.B) {
|
||||||
|
c := make(chan interface{})
|
||||||
|
closed := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
for _ = range c {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
select {
|
||||||
|
case c <- i:
|
||||||
|
case <-closed:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
event/example_test.go
Normal file
42
event/example_test.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func ExampleTypeMux() {
|
||||||
|
type someEvent struct{ I int }
|
||||||
|
type otherEvent struct{ S string }
|
||||||
|
type yetAnotherEvent struct{ X, Y int }
|
||||||
|
|
||||||
|
var mux TypeMux
|
||||||
|
|
||||||
|
// Start a subscriber.
|
||||||
|
done := make(chan struct{})
|
||||||
|
sub := mux.Subscribe(someEvent{}, otherEvent{})
|
||||||
|
go func() {
|
||||||
|
for event := range sub.Chan() {
|
||||||
|
fmt.Printf("Received: %#v\n", event)
|
||||||
|
}
|
||||||
|
fmt.Println("done")
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Post some events.
|
||||||
|
mux.Post(someEvent{5})
|
||||||
|
mux.Post(yetAnotherEvent{X: 3, Y: 4})
|
||||||
|
mux.Post(someEvent{6})
|
||||||
|
mux.Post(otherEvent{"whoa"})
|
||||||
|
|
||||||
|
// Stop closes all subscription channels.
|
||||||
|
// The subscriber goroutine will print "done"
|
||||||
|
// and exit.
|
||||||
|
mux.Stop()
|
||||||
|
|
||||||
|
// Wait for subscriber to return.
|
||||||
|
<-done
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Received: event.someEvent{I:5}
|
||||||
|
// Received: event.someEvent{I:6}
|
||||||
|
// Received: event.otherEvent{S:"whoa"}
|
||||||
|
// done
|
||||||
|
}
|
@ -1,83 +0,0 @@
|
|||||||
package eventer
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
// Basic receiver interface.
|
|
||||||
type Receiver interface {
|
|
||||||
Send(Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver as channel
|
|
||||||
type Channel chan Event
|
|
||||||
|
|
||||||
func (self Channel) Send(ev Event) {
|
|
||||||
self <- ev
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver as function
|
|
||||||
type Function func(ev Event)
|
|
||||||
|
|
||||||
func (self Function) Send(ev Event) {
|
|
||||||
self(ev)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
Type string
|
|
||||||
Data interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Channels map[string][]Receiver
|
|
||||||
|
|
||||||
type EventMachine struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
channels Channels
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *EventMachine {
|
|
||||||
return &EventMachine{channels: make(Channels)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *EventMachine) add(typ string, r Receiver) {
|
|
||||||
self.mu.Lock()
|
|
||||||
self.channels[typ] = append(self.channels[typ], r)
|
|
||||||
self.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generalised methods for the known receiver types
|
|
||||||
// * Channel
|
|
||||||
// * Function
|
|
||||||
func (self *EventMachine) On(typ string, r interface{}) {
|
|
||||||
if eventFunc, ok := r.(func(Event)); ok {
|
|
||||||
self.RegisterFunc(typ, eventFunc)
|
|
||||||
} else if eventChan, ok := r.(Channel); ok {
|
|
||||||
self.RegisterChannel(typ, eventChan)
|
|
||||||
} else {
|
|
||||||
panic("Invalid type for EventMachine::On")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *EventMachine) RegisterChannel(typ string, c Channel) {
|
|
||||||
self.add(typ, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *EventMachine) RegisterFunc(typ string, f Function) {
|
|
||||||
self.add(typ, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *EventMachine) Register(typ string) Channel {
|
|
||||||
c := make(Channel, 1)
|
|
||||||
self.add(typ, c)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *EventMachine) Post(typ string, data interface{}) {
|
|
||||||
self.mu.RLock()
|
|
||||||
if self.channels[typ] != nil {
|
|
||||||
ev := Event{typ, data}
|
|
||||||
for _, receiver := range self.channels[typ] {
|
|
||||||
// Blocking is OK. These are internals and need to be handled
|
|
||||||
receiver.Send(ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.mu.RUnlock()
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
package eventer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChannel(t *testing.T) {
|
|
||||||
eventer := New()
|
|
||||||
|
|
||||||
c := make(Channel, 1)
|
|
||||||
eventer.RegisterChannel("test", c)
|
|
||||||
eventer.Post("test", "hello world")
|
|
||||||
|
|
||||||
res := <-c
|
|
||||||
|
|
||||||
if res.Data.(string) != "hello world" {
|
|
||||||
t.Error("Expected event with data 'hello world'. Got", res.Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFunction(t *testing.T) {
|
|
||||||
eventer := New()
|
|
||||||
|
|
||||||
var data string
|
|
||||||
eventer.RegisterFunc("test", func(ev Event) {
|
|
||||||
data = ev.Data.(string)
|
|
||||||
})
|
|
||||||
eventer.Post("test", "hello world")
|
|
||||||
|
|
||||||
if data != "hello world" {
|
|
||||||
t.Error("Expected event with data 'hello world'. Got", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegister(t *testing.T) {
|
|
||||||
eventer := New()
|
|
||||||
|
|
||||||
c := eventer.Register("test")
|
|
||||||
eventer.Post("test", "hello world")
|
|
||||||
|
|
||||||
res := <-c
|
|
||||||
|
|
||||||
if res.Data.(string) != "hello world" {
|
|
||||||
t.Error("Expected event with data 'hello world'. Got", res.Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOn(t *testing.T) {
|
|
||||||
eventer := New()
|
|
||||||
|
|
||||||
c := make(Channel, 1)
|
|
||||||
eventer.On("test", c)
|
|
||||||
|
|
||||||
var data string
|
|
||||||
eventer.On("test", func(ev Event) {
|
|
||||||
data = ev.Data.(string)
|
|
||||||
})
|
|
||||||
eventer.Post("test", "hello world")
|
|
||||||
|
|
||||||
res := <-c
|
|
||||||
if res.Data.(string) != "hello world" {
|
|
||||||
t.Error("Expected channel event with data 'hello world'. Got", res.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != "hello world" {
|
|
||||||
t.Error("Expected function event with data 'hello world'. Got", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConcurrentUsage(t *testing.T) {
|
|
||||||
rand.Seed(time.Now().Unix())
|
|
||||||
eventer := New()
|
|
||||||
stop := make(chan struct{})
|
|
||||||
recv := make(chan int)
|
|
||||||
poster := func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
eventer.Post("test", "hi")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listener := func(i int) {
|
|
||||||
time.Sleep(time.Duration(rand.Intn(99)) * time.Millisecond)
|
|
||||||
c := eventer.Register("test")
|
|
||||||
// wait for the first event
|
|
||||||
<-c
|
|
||||||
recv <- i
|
|
||||||
// keep receiving to prevent deadlock
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case <-c:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nlisteners := 200
|
|
||||||
go poster()
|
|
||||||
for i := 0; i < nlisteners; i++ {
|
|
||||||
go listener(i)
|
|
||||||
}
|
|
||||||
// wait until everyone has been served
|
|
||||||
for i := 0; i < nlisteners; i++ {
|
|
||||||
<-recv
|
|
||||||
}
|
|
||||||
close(stop)
|
|
||||||
}
|
|
11
events.go
Normal file
11
events.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package eth
|
||||||
|
|
||||||
|
import "container/list"
|
||||||
|
|
||||||
|
type PeerListEvent struct {
|
||||||
|
Peers *list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChainSyncEvent struct {
|
||||||
|
InSync bool
|
||||||
|
}
|
2
peer.go
2
peer.go
@ -802,7 +802,7 @@ func (p *Peer) handleHandshake(msg *ethwire.Msg) {
|
|||||||
p.versionKnown = true
|
p.versionKnown = true
|
||||||
|
|
||||||
p.ethereum.PushPeer(p)
|
p.ethereum.PushPeer(p)
|
||||||
p.ethereum.reactor.Post("peerList", p.ethereum.Peers())
|
p.ethereum.eventMux.Post(PeerListEvent{p.ethereum.Peers()})
|
||||||
|
|
||||||
p.protocolCaps = caps
|
p.protocolCaps = caps
|
||||||
capsIt := caps.NewIterator()
|
capsIt := caps.NewIterator()
|
||||||
|
Loading…
Reference in New Issue
Block a user