From fdd5276d3e77c8abc2b4b90a22170b1b5eb44eda Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 15 Nov 2019 13:32:40 +0300 Subject: [PATCH] network: plug in dBFT library --- config/config.go | 7 + go.mod | 19 +- go.sum | 19 ++ pkg/consensus/block.go | 101 +++++++ pkg/consensus/block_test.go | 48 ++++ pkg/consensus/cache.go | 23 +- pkg/consensus/cache_test.go | 10 +- pkg/consensus/change_view.go | 27 +- pkg/consensus/change_view_test.go | 17 ++ pkg/consensus/commit.go | 21 +- pkg/consensus/commit_test.go | 16 ++ pkg/consensus/consensus.go | 351 ++++++++++++++++++++++++- pkg/consensus/crypto.go | 51 ++++ pkg/consensus/crypto_test.go | 43 +++ pkg/consensus/logger.go | 19 ++ pkg/consensus/payload.go | 206 +++++++++++++-- pkg/consensus/payload_test.go | 174 ++++++++++-- pkg/consensus/prepare_request.go | 59 +++-- pkg/consensus/prepare_request_test.go | 28 ++ pkg/consensus/prepare_response.go | 15 +- pkg/consensus/prepare_response_test.go | 15 ++ pkg/consensus/recovery_message.go | 159 +++++++++-- pkg/consensus/recovery_request.go | 19 +- pkg/consensus/recovery_request_test.go | 14 + pkg/core/block_base.go | 6 +- pkg/core/helper_test.go | 2 +- pkg/network/server.go | 74 +++++- pkg/network/server_config.go | 9 + pkg/wallet/account.go | 5 + 29 files changed, 1415 insertions(+), 142 deletions(-) create mode 100644 pkg/consensus/block.go create mode 100644 pkg/consensus/block_test.go create mode 100644 pkg/consensus/change_view_test.go create mode 100644 pkg/consensus/commit_test.go create mode 100644 pkg/consensus/crypto.go create mode 100644 pkg/consensus/crypto_test.go create mode 100644 pkg/consensus/logger.go create mode 100644 pkg/consensus/prepare_request_test.go create mode 100644 pkg/consensus/prepare_response_test.go create mode 100644 pkg/consensus/recovery_request_test.go diff --git a/config/config.go b/config/config.go index adc038951..a96b9dac2 100644 --- a/config/config.go +++ b/config/config.go @@ -78,6 +78,13 @@ type ( MinPeers int `yaml:"MinPeers"` Monitoring metrics.PrometheusConfig `yaml:"Monitoring"` RPC RPCConfig `yaml:"RPC"` + UnlockWallet WalletConfig `yaml:"UnlockWallet"` + } + + // WalletConfig is a wallet info. + WalletConfig struct { + Path string `yaml:"Path"` + Password string `yaml:"Password"` } // RPCConfig is an RPC service configuration information (to be moved to the rpc package, see #423). diff --git a/go.mod b/go.mod index 668cc6bc4..b11d62b19 100644 --- a/go.mod +++ b/go.mod @@ -2,31 +2,22 @@ module github.com/CityOfZion/neo-go require ( github.com/Workiva/go-datastructures v1.0.50 - github.com/abiosoft/ishell v2.0.0+incompatible // indirect - github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect - github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect github.com/alicebob/miniredis v2.5.0+incompatible github.com/etcd-io/bbolt v1.3.3 - github.com/fatih/color v1.7.0 // indirect - github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/go-redis/redis v6.10.2+incompatible github.com/go-yaml/yaml v2.1.0+incompatible - github.com/golang/snappy v0.0.0-20170215233205-553a64147049 // indirect - github.com/gomodule/redigo v2.0.0+incompatible // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.9 // indirect github.com/mr-tron/base58 v1.1.2 + github.com/nspcc-dev/dbft v0.0.0-20191125155719-1c35ab053055 github.com/nspcc-dev/rfc6979 v0.1.0 - github.com/onsi/gomega v1.4.2 // indirect github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v1.2.1 github.com/sirupsen/logrus v1.4.2 - github.com/stretchr/testify v1.3.0 + github.com/stretchr/testify v1.4.0 github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73 github.com/urfave/cli v1.20.0 - github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036 // indirect - go.etcd.io/bbolt v1.3.3 // indirect - golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 + go.uber.org/atomic v1.4.0 + go.uber.org/zap v1.10.0 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 golang.org/x/text v0.3.0 golang.org/x/tools v0.0.0-20180318012157-96caea41033d gopkg.in/abiosoft/ishell.v2 v2.0.0 diff --git a/go.sum b/go.sum index 064e11c8a..934dd73ba 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/CityOfZion/neo-go v0.62.1-pre.0.20191114145240-e740fbe708f8/go.mod h1:MJCkWUBhi9pn/CrYO1Q3P687y2KeahrOPS9BD9LDGb0= github.com/Workiva/go-datastructures v1.0.50 h1:slDmfW6KCHcC7U+LP3DDBbm4fqTwZGn1beOFPfGaLvo= github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= @@ -81,6 +82,10 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mr-tron/base58 v1.1.2 h1:ZEw4I2EgPKDJ2iEw0cNmLB3ROrEmkOtXIkaG7wZg+78= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nspcc-dev/dbft v0.0.0-20191125155719-1c35ab053055 h1:Rs8zeSFfXOX2RuUjWwsUrQpNbxoWyeXU8lWPlYkxDo0= +github.com/nspcc-dev/dbft v0.0.0-20191125155719-1c35ab053055/go.mod h1:w1Ln2aT+dBlPhLnuZhBV+DfPEdS2CHWWLp5JTScY3bw= +github.com/nspcc-dev/neofs-crypto v0.2.0 h1:ftN+59WqxSWz/RCgXYOfhmltOOqU+udsNQSvN6wkFck= +github.com/nspcc-dev/neofs-crypto v0.2.0/go.mod h1:F/96fUzPM3wR+UGsPi3faVNmFlA9KAEAUQR7dMxZmNA= github.com/nspcc-dev/rfc6979 v0.1.0 h1:Lwg7esRRoyK1Up/IN1vAef1EmvrBeMHeeEkek2fAJ6c= github.com/nspcc-dev/rfc6979 v0.1.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= @@ -111,11 +116,15 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73 h1:I2drr5K0tykBofr74ZEGliE/Hf6fNkEbcPyFvsy7wZk= github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= @@ -124,12 +133,21 @@ github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036 h1:1b6PAtenNyhsmo/ github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= @@ -143,6 +161,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/consensus/block.go b/pkg/consensus/block.go new file mode 100644 index 000000000..6d9c6dc27 --- /dev/null +++ b/pkg/consensus/block.go @@ -0,0 +1,101 @@ +package consensus + +import ( + "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/nspcc-dev/dbft/block" + "github.com/nspcc-dev/dbft/crypto" +) + +// neoBlock is a wrapper of core.Block which implements +// methods necessary for dBFT library. +type neoBlock struct { + core.Block + + signature []byte +} + +var _ block.Block = (*neoBlock)(nil) + +// Sign implements block.Block interface. +func (n *neoBlock) Sign(key crypto.PrivateKey) error { + data := n.BlockBase.GetHashableData() + sig, err := key.Sign(data[:]) + if err != nil { + return err + } + + n.signature = sig + + return nil +} + +// Verify implements block.Block interface. +func (n *neoBlock) Verify(key crypto.PublicKey, sign []byte) error { + data := n.BlockBase.GetHashableData() + return key.Verify(data, sign) +} + +// Transactions implements block.Block interface. +func (n *neoBlock) Transactions() []block.Transaction { + txes := make([]block.Transaction, len(n.Block.Transactions)) + for i, tx := range n.Block.Transactions { + txes[i] = tx + } + + return txes +} + +// SetTransactions implements block.Block interface. +func (n *neoBlock) SetTransactions(txes []block.Transaction) { + n.Block.Transactions = make([]*transaction.Transaction, len(txes)) + for i, tx := range txes { + n.Block.Transactions[i] = tx.(*transaction.Transaction) + } +} + +// Version implements block.Block interface. +func (n *neoBlock) Version() uint32 { return n.Block.Version } + +// SetVersion implements block.Block interface. +func (n *neoBlock) SetVersion(v uint32) { n.Block.Version = v } + +// PrevHash implements block.Block interface. +func (n *neoBlock) PrevHash() util.Uint256 { return n.Block.PrevHash } + +// SetPrevHash implements block.Block interface. +func (n *neoBlock) SetPrevHash(h util.Uint256) { n.Block.PrevHash = h } + +// MerkleRoot implements block.Block interface. +func (n *neoBlock) MerkleRoot() util.Uint256 { return n.Block.MerkleRoot } + +// SetMerkleRoot implements block.Block interface. +func (n *neoBlock) SetMerkleRoot(r util.Uint256) { n.Block.MerkleRoot = r } + +// Timestamp implements block.Block interface. +func (n *neoBlock) Timestamp() uint32 { return n.Block.Timestamp } + +// SetTimestamp implements block.Block interface. +func (n *neoBlock) SetTimestamp(ts uint32) { n.Block.Timestamp = ts } + +// Index implements block.Block interface. +func (n *neoBlock) Index() uint32 { return n.Block.Index } + +// SetIndex implements block.Block interface. +func (n *neoBlock) SetIndex(i uint32) { n.Block.Index = i } + +// ConsensusData implements block.Block interface. +func (n *neoBlock) ConsensusData() uint64 { return n.Block.ConsensusData } + +// SetConsensusData implements block.Block interface. +func (n *neoBlock) SetConsensusData(nonce uint64) { n.Block.ConsensusData = nonce } + +// NextConsensus implements block.Block interface. +func (n *neoBlock) NextConsensus() util.Uint160 { return n.Block.NextConsensus } + +// SetNextConsensus implements block.Block interface. +func (n *neoBlock) SetNextConsensus(h util.Uint160) { n.Block.NextConsensus = h } + +// Signature implements block.Block interface. +func (n *neoBlock) Signature() []byte { return n.signature } diff --git a/pkg/consensus/block_test.go b/pkg/consensus/block_test.go new file mode 100644 index 000000000..fda210402 --- /dev/null +++ b/pkg/consensus/block_test.go @@ -0,0 +1,48 @@ +package consensus + +import ( + "crypto/rand" + "testing" + + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/nspcc-dev/dbft/block" + "github.com/nspcc-dev/dbft/crypto" + "github.com/stretchr/testify/require" +) + +func TestNeoBlock_Sign(t *testing.T) { + b := new(neoBlock) + priv, pub := crypto.Generate(rand.Reader) + + require.NoError(t, b.Sign(priv)) + require.NoError(t, b.Verify(pub, b.Signature())) +} + +func TestNeoBlock_Setters(t *testing.T) { + b := new(neoBlock) + + b.SetVersion(1) + require.EqualValues(t, 1, b.Version()) + + b.SetIndex(12) + require.EqualValues(t, 12, b.Index()) + + b.SetTimestamp(777) + require.EqualValues(t, 777, b.Timestamp()) + + b.SetConsensusData(456) + require.EqualValues(t, 456, b.ConsensusData()) + + b.SetMerkleRoot(util.Uint256{1, 2, 3, 4}) + require.Equal(t, util.Uint256{1, 2, 3, 4}, b.MerkleRoot()) + + b.SetNextConsensus(util.Uint160{9, 2}) + require.Equal(t, util.Uint160{9, 2}, b.NextConsensus()) + + b.SetPrevHash(util.Uint256{9, 8, 7}) + require.Equal(t, util.Uint256{9, 8, 7}, b.PrevHash()) + + txx := []block.Transaction{newMinerTx(123)} + b.SetTransactions(txx) + require.Equal(t, txx, b.Transactions()) +} diff --git a/pkg/consensus/cache.go b/pkg/consensus/cache.go index 39af2db31..3ff86ff04 100644 --- a/pkg/consensus/cache.go +++ b/pkg/consensus/cache.go @@ -17,6 +17,11 @@ type relayCache struct { queue *list.List } +// hashable is a type of items which can be stored in the relayCache. +type hashable interface { + Hash() util.Uint256 +} + func newFIFOCache(capacity int) *relayCache { return &relayCache{ RWMutex: new(sync.RWMutex), @@ -28,7 +33,7 @@ func newFIFOCache(capacity int) *relayCache { } // Add adds payload into a cache if it doesn't already exist. -func (c *relayCache) Add(p *Payload) { +func (c *relayCache) Add(p hashable) { c.Lock() defer c.Unlock() @@ -40,21 +45,29 @@ func (c *relayCache) Add(p *Payload) { if c.queue.Len() >= c.maxCap { first := c.queue.Front() c.queue.Remove(first) - delete(c.elems, first.Value.(*Payload).Hash()) + delete(c.elems, first.Value.(hashable).Hash()) } e := c.queue.PushBack(p) c.elems[h] = e } +// Has checks if an item is already in cache. +func (c *relayCache) Has(h util.Uint256) bool { + c.RLock() + defer c.RUnlock() + + return c.elems[h] != nil +} + // Get returns payload with the specified hash from cache. -func (c *relayCache) Get(h util.Uint256) *Payload { +func (c *relayCache) Get(h util.Uint256) hashable { c.RLock() defer c.RUnlock() e, ok := c.elems[h] if !ok { - return nil + return hashable(nil) } - return e.Value.(*Payload) + return e.Value.(hashable) } diff --git a/pkg/consensus/cache_test.go b/pkg/consensus/cache_test.go index 631cc70d9..9e1529bfd 100644 --- a/pkg/consensus/cache_test.go +++ b/pkg/consensus/cache_test.go @@ -3,6 +3,7 @@ package consensus import ( "testing" + "github.com/nspcc-dev/dbft/payload" "github.com/stretchr/testify/require" ) @@ -15,6 +16,7 @@ func TestRelayCache_Add(t *testing.T) { for i := 1; i < capacity; i++ { c.Add(&payloads[i]) + require.True(t, c.Has(payloads[i].Hash())) require.Equal(t, i, c.queue.Len()) require.Equal(t, i, len(c.elems)) } @@ -40,7 +42,7 @@ func TestRelayCache_Add(t *testing.T) { } // oldest payload was removed - require.Equal(t, (*Payload)(nil), c.Get(payloads[1].Hash())) + require.Equal(t, nil, c.Get(payloads[1].Hash())) } func getDifferentPayloads(t *testing.T, n int) (payloads []Payload) { @@ -49,10 +51,10 @@ func getDifferentPayloads(t *testing.T, n int) (payloads []Payload) { var sign [signatureSize]byte fillRandom(t, sign[:]) - payloads[i].ValidatorIndex = uint16(i) - payloads[i].Type = commitType + payloads[i].SetValidatorIndex(uint16(i)) + payloads[i].SetType(payload.MessageType(commitType)) payloads[i].payload = &commit{ - Signature: sign, + signature: sign, } } diff --git a/pkg/consensus/change_view.go b/pkg/consensus/change_view.go index 4274a38d2..c47b42485 100644 --- a/pkg/consensus/change_view.go +++ b/pkg/consensus/change_view.go @@ -1,19 +1,36 @@ package consensus -import "github.com/CityOfZion/neo-go/pkg/io" +import ( + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/nspcc-dev/dbft/payload" +) // changeView represents dBFT ChangeView message. type changeView struct { - NewViewNumber byte - Timestamp uint32 + newViewNumber byte + timestamp uint32 } +var _ payload.ChangeView = (*changeView)(nil) + // EncodeBinary implements io.Serializable interface. func (c *changeView) EncodeBinary(w *io.BinWriter) { - w.WriteLE(c.Timestamp) + w.WriteLE(c.timestamp) } // DecodeBinary implements io.Serializable interface. func (c *changeView) DecodeBinary(r *io.BinReader) { - r.ReadLE(&c.Timestamp) + r.ReadLE(&c.timestamp) } + +// NewViewNumber implements payload.ChangeView interface. +func (c changeView) NewViewNumber() byte { return c.newViewNumber } + +// SetNewViewNumber implements payload.ChangeView interface. +func (c *changeView) SetNewViewNumber(view byte) { c.newViewNumber = view } + +// Timestamp implements payload.ChangeView interface. +func (c changeView) Timestamp() uint32 { return c.timestamp } + +// SetTimestamp implements payload.ChangeView interface. +func (c *changeView) SetTimestamp(ts uint32) { c.timestamp = ts } diff --git a/pkg/consensus/change_view_test.go b/pkg/consensus/change_view_test.go new file mode 100644 index 000000000..4d1142f39 --- /dev/null +++ b/pkg/consensus/change_view_test.go @@ -0,0 +1,17 @@ +package consensus + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestChangeView_Setters(t *testing.T) { + var c changeView + + c.SetTimestamp(123) + require.EqualValues(t, 123, c.Timestamp()) + + c.SetNewViewNumber(2) + require.EqualValues(t, 2, c.NewViewNumber()) +} diff --git a/pkg/consensus/commit.go b/pkg/consensus/commit.go index 2d3acac08..e5d0e0fb9 100644 --- a/pkg/consensus/commit.go +++ b/pkg/consensus/commit.go @@ -1,22 +1,35 @@ package consensus -import "github.com/CityOfZion/neo-go/pkg/io" +import ( + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/nspcc-dev/dbft/payload" +) // commit represents dBFT Commit message. type commit struct { - Signature [signatureSize]byte + signature [signatureSize]byte } // signatureSize is an rfc6989 signature size in bytes // without leading byte (0x04, uncompressed) const signatureSize = 64 +var _ payload.Commit = (*commit)(nil) + // EncodeBinary implements io.Serializable interface. func (c *commit) EncodeBinary(w *io.BinWriter) { - w.WriteBE(c.Signature) + w.WriteBE(c.signature) } // DecodeBinary implements io.Serializable interface. func (c *commit) DecodeBinary(r *io.BinReader) { - r.ReadBE(&c.Signature) + r.ReadBE(&c.signature) +} + +// Signature implements payload.Commit interface. +func (c commit) Signature() []byte { return c.signature[:] } + +// SetSignature implements payload.Commit interface. +func (c *commit) SetSignature(signature []byte) { + copy(c.signature[:], signature) } diff --git a/pkg/consensus/commit_test.go b/pkg/consensus/commit_test.go new file mode 100644 index 000000000..0cefb4e2a --- /dev/null +++ b/pkg/consensus/commit_test.go @@ -0,0 +1,16 @@ +package consensus + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCommit_Setters(t *testing.T) { + var sign [signatureSize]byte + fillRandom(t, sign[:]) + + var c commit + c.SetSignature(sign[:]) + require.Equal(t, sign[:], c.Signature()) +} diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index b76bd8b91..3c9e87dc6 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -1,34 +1,371 @@ package consensus -import "github.com/CityOfZion/neo-go/pkg/util" +import ( + "errors" + "math/rand" + "sort" + "time" + + "github.com/CityOfZion/neo-go/config" + "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/CityOfZion/neo-go/pkg/wallet" + "github.com/nspcc-dev/dbft" + "github.com/nspcc-dev/dbft/block" + "github.com/nspcc-dev/dbft/crypto" + "github.com/nspcc-dev/dbft/payload" + "go.uber.org/zap" +) // cacheMaxCapacity is the default cache capacity taken // from C# implementation https://github.com/neo-project/neo/blob/master/neo/Ledger/Blockchain.cs#L64 const cacheMaxCapacity = 100 +// defaultTimePerBlock is a period between blocks which is used in NEO. +const defaultTimePerBlock = 15 * time.Second + // Service represents consensus instance. type Service interface { + // Start initializes dBFT and starts event loop for consensus service. + // It must be called only when sufficient amount of peers are connected. + Start() + + // OnPayload is a callback to notify Service about new received payload. OnPayload(p *Payload) + // OnTransaction is a callback to notify Service about new received transaction. + OnTransaction(tx *transaction.Transaction) + // GetPayload returns Payload with specified hash if it is present in the local cache. GetPayload(h util.Uint256) *Payload } type service struct { + Config + + log *zap.SugaredLogger + // cache is a fifo cache which stores recent payloads. cache *relayCache + // txx is a fifo cache which stores miner transactions. + txx *relayCache + dbft *dbft.DBFT + // messages and transactions are channels needed to process + // everything in single thread. + messages chan Payload + transactions chan *transaction.Transaction +} + +// Config is a configuration for consensus services. +type Config struct { + // Broadcast is a callback which is called to notify server + // about new consensus payload to sent. + Broadcast func(p *Payload) + // Chain is a core.Blockchainer instance. + Chain core.Blockchainer + // RequestTx is a callback to which will be called + // when a node lacks transactions present in a block. + RequestTx func(h ...util.Uint256) + // TimePerBlock minimal time that should pass before next block is accepted. + TimePerBlock time.Duration + // Wallet is a local-node wallet configuration. + Wallet *config.WalletConfig } // NewService returns new consensus.Service instance. -func NewService() Service { - return &service{ - cache: newFIFOCache(cacheMaxCapacity), +func NewService(cfg Config) (Service, error) { + log, err := getLogger() + if err != nil { + return nil, err + } + + if cfg.TimePerBlock <= 0 { + cfg.TimePerBlock = defaultTimePerBlock + } + + srv := &service{ + Config: cfg, + + log: log.Sugar(), + cache: newFIFOCache(cacheMaxCapacity), + txx: newFIFOCache(cacheMaxCapacity), + messages: make(chan Payload, 100), + } + + if cfg.Wallet == nil { + return srv, nil + } + + priv, pub := getKeyPair(cfg.Wallet) + + srv.dbft = dbft.New( + dbft.WithLogger(srv.log.Desugar()), + dbft.WithSecondsPerBlock(cfg.TimePerBlock), + dbft.WithKeyPair(priv, pub), + dbft.WithTxPerBlock(10000), + dbft.WithRequestTx(cfg.RequestTx), + dbft.WithGetTx(srv.getTx), + dbft.WithGetVerified(srv.getVerifiedTx), + dbft.WithBroadcast(srv.broadcast), + dbft.WithProcessBlock(srv.processBlock), + dbft.WithVerifyBlock(srv.verifyBlock), + dbft.WithGetBlock(srv.getBlock), + dbft.WithWatchOnly(func() bool { return false }), + dbft.WithNewBlock(func() block.Block { return new(neoBlock) }), + dbft.WithCurrentHeight(cfg.Chain.BlockHeight), + dbft.WithCurrentBlockHash(cfg.Chain.CurrentBlockHash), + dbft.WithGetValidators(srv.getValidators), + dbft.WithGetConsensusAddress(srv.getConsensusAddress), + + dbft.WithNewConsensusPayload(func() payload.ConsensusPayload { return new(Payload) }), + dbft.WithNewPrepareRequest(func() payload.PrepareRequest { return new(prepareRequest) }), + dbft.WithNewPrepareResponse(func() payload.PrepareResponse { return new(prepareResponse) }), + dbft.WithNewChangeView(func() payload.ChangeView { return new(changeView) }), + dbft.WithNewCommit(func() payload.Commit { return new(commit) }), + ) + + if srv.dbft == nil { + return nil, errors.New("can't initialize dBFT") + } + + return srv, nil +} + +var ( + _ block.Transaction = (*transaction.Transaction)(nil) + _ block.Block = (*neoBlock)(nil) +) + +func (s *service) Start() { + s.dbft.Start() + + go s.eventLoop() +} + +func (s *service) eventLoop() { + for { + select { + case hv := <-s.dbft.Timer.C(): + s.log.Debugf("timer fired (%d,%d)", hv.Height, hv.View) + s.dbft.OnTimeout(hv) + case msg := <-s.messages: + s.log.Debugf("received message from %d", msg.validatorIndex) + s.dbft.OnReceive(&msg) + case tx := <-s.transactions: + s.dbft.OnTransaction(tx) + } } } +func getKeyPair(cfg *config.WalletConfig) (crypto.PrivateKey, crypto.PublicKey) { + acc, err := wallet.DecryptAccount(cfg.Path, cfg.Password) + if err != nil { + return nil, nil + } + + key := acc.PrivateKey() + if key == nil { + return nil, nil + } + + return &privateKey{PrivateKey: key}, &publicKey{PublicKey: key.PublicKey()} +} + // OnPayload handles Payload receive. -func (s *service) OnPayload(p *Payload) { - s.cache.Add(p) +func (s *service) OnPayload(cp *Payload) { + if s.cache.Has(cp.Hash()) { + return + } + + s.Config.Broadcast(cp) + s.cache.Add(cp) + + if s.dbft == nil { + return + } + + // we use switch here because other payloads could be possibly added in future + switch cp.Type() { + case payload.PrepareRequestType: + s.txx.Add(&cp.GetPrepareRequest().(*prepareRequest).minerTx) + } + + s.messages <- *cp +} + +func (s *service) OnTransaction(tx *transaction.Transaction) { + if s.dbft != nil { + s.transactions <- tx + } } // GetPayload returns payload stored in cache. func (s *service) GetPayload(h util.Uint256) *Payload { - return s.cache.Get(h) + p := s.cache.Get(h) + if p == nil { + return (*Payload)(nil) + } + + cp := *p.(*Payload) + + return &cp +} + +func (s *service) broadcast(p payload.ConsensusPayload) { + switch p.Type() { + case payload.PrepareRequestType: + pr := p.GetPrepareRequest().(*prepareRequest) + pr.minerTx = *s.txx.Get(pr.transactionHashes[0]).(*transaction.Transaction) + } + + s.cache.Add(p) + s.Config.Broadcast(p.(*Payload)) +} + +func (s *service) getTx(h util.Uint256) block.Transaction { + if tx := s.txx.Get(h); tx != nil { + return tx.(*transaction.Transaction) + } + + tx, _, _ := s.Config.Chain.GetTransaction(h) + + return tx +} + +func (s *service) verifyBlock(b block.Block) bool { + coreb := &b.(*neoBlock).Block + for _, tx := range coreb.Transactions { + if err := s.Chain.VerifyTx(tx, coreb); err != nil { + return false + } + } + + return true +} + +func (s *service) processBlock(b block.Block) { + bb := &b.(*neoBlock).Block + bb.Script = s.getBlockWitness(bb) + + if err := s.Chain.AddBlock(bb); err != nil { + s.log.Warnf("error on add block: %v", err) + } +} + +func (s *service) getBlockWitness(b *core.Block) *transaction.Witness { + dctx := s.dbft.Context + pubs := convertKeys(dctx.Validators) + sigs := make(map[*keys.PublicKey][]byte) + + for i := range pubs { + if p := dctx.CommitPayloads[i]; p != nil && p.ViewNumber() == dctx.ViewNumber { + sigs[pubs[i]] = p.GetCommit().Signature() + } + } + + m := s.dbft.Context.M() + verif, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) + if err != nil { + s.log.Warnf("can't create multisig redeem script: %v", err) + return nil + } + + sort.Sort(keys.PublicKeys(pubs)) + + var invoc []byte + for i, j := 0, 0; i < len(pubs) && j < m; i++ { + if sig, ok := sigs[pubs[i]]; ok { + invoc = append(invoc, byte(vm.PUSHBYTES64)) + invoc = append(invoc, sig...) + j++ + } + } + + return &transaction.Witness{ + InvocationScript: invoc, + VerificationScript: verif, + } +} + +func (s *service) getBlock(h util.Uint256) block.Block { + b, err := s.Chain.GetBlock(h) + if err != nil { + return nil + } + + return &neoBlock{Block: *b} +} + +func (s *service) getVerifiedTx(count int) []block.Transaction { + pool := s.Config.Chain.GetMemPool() + txx := pool.GetVerifiedTransactions() + + res := make([]block.Transaction, len(txx)+1) + for i := 1; i < len(res); i++ { + res[i] = txx[i] + } + + for { + nonce := rand.Uint32() + res[0] = &transaction.Transaction{ + Type: transaction.MinerType, + Version: 0, + Data: &transaction.MinerTX{Nonce: nonce}, + Attributes: nil, + Inputs: nil, + Outputs: nil, + Scripts: nil, + Trimmed: false, + } + + if tx, _, _ := s.Chain.GetTransaction(res[0].Hash()); tx == nil { + break + } + } + + s.txx.Add(res[0]) + + return res +} + +func (s *service) getValidators(txx ...block.Transaction) []crypto.PublicKey { + var pKeys []*keys.PublicKey + if len(txx) == 0 { + pKeys, _ = s.Chain.GetValidators() + } else { + ntxx := make([]*transaction.Transaction, len(txx)) + for i := range ntxx { + ntxx[i] = txx[i].(*transaction.Transaction) + } + + pKeys, _ = s.Chain.GetValidators(ntxx...) + } + + pubs := make([]crypto.PublicKey, len(pKeys)) + for i := range pKeys { + pubs[i] = &publicKey{PublicKey: pKeys[i]} + } + + return pubs +} + +func (s *service) getConsensusAddress(validators ...crypto.PublicKey) (h util.Uint160) { + pubs := convertKeys(validators) + + script, err := smartcontract.CreateMultiSigRedeemScript(s.dbft.M(), pubs) + if err != nil { + return + } + + return crypto.Hash160(script) +} + +func convertKeys(validators []crypto.PublicKey) (pubs []*keys.PublicKey) { + pubs = make([]*keys.PublicKey, len(validators)) + for i, k := range validators { + pubs[i] = k.(*publicKey).PublicKey + } + + return } diff --git a/pkg/consensus/crypto.go b/pkg/consensus/crypto.go new file mode 100644 index 000000000..97f97fdc4 --- /dev/null +++ b/pkg/consensus/crypto.go @@ -0,0 +1,51 @@ +package consensus + +import ( + "crypto/sha256" + "errors" + + "github.com/CityOfZion/neo-go/pkg/crypto/keys" +) + +// privateKey is a wrapper around keys.PrivateKey +// which implements crypto.PrivateKey interface. +type privateKey struct { + *keys.PrivateKey +} + +// MarshalBinary implements encoding.BinaryMarshaler interface. +func (p privateKey) MarshalBinary() (data []byte, err error) { + return p.PrivateKey.Bytes(), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler interface. +func (p *privateKey) UnmarshalBinary(data []byte) (err error) { + p.PrivateKey, err = keys.NewPrivateKeyFromBytes(data) + return +} + +// publicKey is a wrapper around keys.PublicKey +// which implements crypto.PublicKey interface. +type publicKey struct { + *keys.PublicKey +} + +// MarshalBinary implements encoding.BinaryMarshaler interface. +func (p publicKey) MarshalBinary() (data []byte, err error) { + return p.PublicKey.Bytes(), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler interface. +func (p *publicKey) UnmarshalBinary(data []byte) error { + return p.PublicKey.DecodeBytes(data) +} + +// Verify implements crypto.PublicKey interface. +func (p publicKey) Verify(msg, sig []byte) error { + hash := sha256.Sum256(msg) + if p.PublicKey.Verify(sig, hash[:]) { + return nil + } + + return errors.New("error") +} diff --git a/pkg/consensus/crypto_test.go b/pkg/consensus/crypto_test.go new file mode 100644 index 000000000..9c7405343 --- /dev/null +++ b/pkg/consensus/crypto_test.go @@ -0,0 +1,43 @@ +package consensus + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" +) + +func TestCrypt(t *testing.T) { + key, err := keys.NewPrivateKey() + require.NoError(t, err) + + priv := privateKey{key} + data, err := priv.MarshalBinary() + require.NoError(t, err) + + key1, err := keys.NewPrivateKey() + require.NoError(t, err) + + priv1 := privateKey{key1} + require.NotEqual(t, priv, priv1) + require.NoError(t, priv1.UnmarshalBinary(data)) + require.Equal(t, priv, priv1) + + pub := publicKey{key.PublicKey()} + data, err = pub.MarshalBinary() + require.NoError(t, err) + + pub1 := publicKey{key1.PublicKey()} + require.NotEqual(t, pub, pub1) + require.NoError(t, pub1.UnmarshalBinary(data)) + require.Equal(t, pub, pub1) + + data = []byte{1, 2, 3, 4} + + sign, err := priv.Sign(data) + require.NoError(t, err) + require.NoError(t, pub.Verify(data, sign)) + + sign[0] = ^sign[0] + require.Error(t, pub.Verify(data, sign)) +} diff --git a/pkg/consensus/logger.go b/pkg/consensus/logger.go new file mode 100644 index 000000000..49dba8e69 --- /dev/null +++ b/pkg/consensus/logger.go @@ -0,0 +1,19 @@ +package consensus + +import ( + "go.uber.org/zap" +) + +func getLogger() (*zap.Logger, error) { + cc := zap.NewDevelopmentConfig() + cc.DisableCaller = true + cc.DisableStacktrace = true + cc.Encoding = "console" + + log, err := cc.Build() + if err != nil { + return nil, err + } + + return log.With(zap.String("module", "dbft")), nil +} diff --git a/pkg/consensus/payload.go b/pkg/consensus/payload.go index 5abdef1aa..6740e9370 100644 --- a/pkg/consensus/payload.go +++ b/pkg/consensus/payload.go @@ -1,12 +1,16 @@ package consensus import ( + "crypto/sha256" "fmt" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/nspcc-dev/dbft/payload" "github.com/pkg/errors" ) @@ -24,11 +28,11 @@ type ( Payload struct { message - Version uint32 - ValidatorIndex uint16 - PrevHash util.Uint256 - Height uint32 - Timestamp uint32 + version uint32 + validatorIndex uint16 + prevHash util.Uint256 + height uint32 + timestamp uint32 Witness transaction.Witness } @@ -43,13 +47,125 @@ const ( recoveryMessageType messageType = 0x41 ) +// ViewNumber implements payload.ConsensusPayload interface. +func (p Payload) ViewNumber() byte { + return p.message.ViewNumber +} + +// SetViewNumber implements payload.ConsensusPayload interface. +func (p *Payload) SetViewNumber(view byte) { + p.message.ViewNumber = view +} + +// Type implements payload.ConsensusPayload interface. +func (p Payload) Type() payload.MessageType { + return payload.MessageType(p.message.Type) +} + +// SetType implements payload.ConsensusPayload interface. +func (p *Payload) SetType(t payload.MessageType) { + p.message.Type = messageType(t) +} + +// Payload implements payload.ConsensusPayload interface. +func (p Payload) Payload() interface{} { + return p.payload +} + +// SetPayload implements payload.ConsensusPayload interface. +func (p *Payload) SetPayload(pl interface{}) { + p.payload = pl.(io.Serializable) +} + +// GetChangeView implements payload.ConsensusPayload interface. +func (p Payload) GetChangeView() payload.ChangeView { return p.payload.(payload.ChangeView) } + +// GetPrepareRequest implements payload.ConsensusPayload interface. +func (p Payload) GetPrepareRequest() payload.PrepareRequest { + return p.payload.(payload.PrepareRequest) +} + +// GetPrepareResponse implements payload.ConsensusPayload interface. +func (p Payload) GetPrepareResponse() payload.PrepareResponse { + return p.payload.(payload.PrepareResponse) +} + +// GetCommit implements payload.ConsensusPayload interface. +func (p Payload) GetCommit() payload.Commit { return p.payload.(payload.Commit) } + +// GetRecoveryRequest implements payload.ConsensusPayload interface. +func (p Payload) GetRecoveryRequest() payload.RecoveryRequest { + return p.payload.(payload.RecoveryRequest) +} + +// GetRecoveryMessage implements payload.ConsensusPayload interface. +func (p Payload) GetRecoveryMessage() payload.RecoveryMessage { + return p.payload.(payload.RecoveryMessage) +} + +// MarshalUnsigned implements payload.ConsensusPayload interface. +func (p Payload) MarshalUnsigned() []byte { + w := io.NewBufBinWriter() + p.EncodeBinaryUnsigned(w.BinWriter) + + return w.Bytes() +} + +// UnmarshalUnsigned implements payload.ConsensusPayload interface. +func (p *Payload) UnmarshalUnsigned(data []byte) error { + r := io.NewBinReaderFromBuf(data) + p.DecodeBinaryUnsigned(r) + + return r.Err +} + +// Version implements payload.ConsensusPayload interface. +func (p Payload) Version() uint32 { + return p.version +} + +// SetVersion implements payload.ConsensusPayload interface. +func (p *Payload) SetVersion(v uint32) { + p.version = v +} + +// ValidatorIndex implements payload.ConsensusPayload interface. +func (p Payload) ValidatorIndex() uint16 { + return p.validatorIndex +} + +// SetValidatorIndex implements payload.ConsensusPayload interface. +func (p *Payload) SetValidatorIndex(i uint16) { + p.validatorIndex = i +} + +// PrevHash implements payload.ConsensusPayload interface. +func (p Payload) PrevHash() util.Uint256 { + return p.prevHash +} + +// SetPrevHash implements payload.ConsensusPayload interface. +func (p *Payload) SetPrevHash(h util.Uint256) { + p.prevHash = h +} + +// Height implements payload.ConsensusPayload interface. +func (p Payload) Height() uint32 { + return p.height +} + +// SetHeight implements payload.ConsensusPayload interface. +func (p *Payload) SetHeight(h uint32) { + p.height = h +} + // EncodeBinaryUnsigned writes payload to w excluding signature. -func (p *Payload) EncodeBinaryUnsigned(w *io.BinWriter) { - w.WriteLE(p.Version) - w.WriteBE(p.PrevHash[:]) - w.WriteLE(p.Height) - w.WriteLE(p.ValidatorIndex) - w.WriteLE(p.Timestamp) +func (p Payload) EncodeBinaryUnsigned(w *io.BinWriter) { + w.WriteLE(p.version) + w.WriteBE(p.prevHash[:]) + w.WriteLE(p.height) + w.WriteLE(p.validatorIndex) + w.WriteLE(p.timestamp) ww := io.NewBufBinWriter() p.message.EncodeBinary(ww.BinWriter) @@ -64,20 +180,59 @@ func (p *Payload) EncodeBinary(w *io.BinWriter) { p.Witness.EncodeBinary(w) } -// DecodeBinaryUnsigned reads payload from w excluding signature. -func (p *Payload) DecodeBinaryUnsigned(r *io.BinReader) { - r.ReadLE(&p.Version) - r.ReadBE(p.PrevHash[:]) - r.ReadLE(&p.Height) - r.ReadLE(&p.ValidatorIndex) - r.ReadLE(&p.Timestamp) +// Sign signs payload using the private key. +// It also sets corresponding verification and invocation scripts. +func (p *Payload) Sign(key *privateKey) error { + sig, err := key.Sign(p.MarshalUnsigned()) + if err != nil { + return err + } - data := r.ReadBytes() - rr := io.NewBinReaderFromBuf(data) - p.message.DecodeBinary(rr) + verif, err := smartcontract.CreateSignatureRedeemScript(key.PublicKey()) + if err != nil { + return err + } + + p.Witness.InvocationScript = append([]byte{byte(vm.PUSHBYTES64)}, sig...) + p.Witness.VerificationScript = verif + + return nil } -// Hash returns 32-byte message hash. +// Verify verifies payload using provided Witness. +func (p *Payload) Verify() bool { + h := sha256.Sum256(p.MarshalUnsigned()) + v := vm.New() + v.SetCheckedHash(h[:]) + v.Load(append(p.Witness.InvocationScript, p.Witness.VerificationScript...)) + if err := v.Run(); err != nil || v.Estack().Len() == 0 { + return false + } + + result, err := v.Estack().Top().TryBool() + + return err == nil && result +} + +// DecodeBinaryUnsigned reads payload from w excluding signature. +func (p *Payload) DecodeBinaryUnsigned(r *io.BinReader) { + r.ReadLE(&p.version) + r.ReadBE(p.prevHash[:]) + r.ReadLE(&p.height) + r.ReadLE(&p.validatorIndex) + r.ReadLE(&p.timestamp) + + data := r.ReadBytes() + if r.Err != nil { + return + } + + rr := io.NewBinReaderFromBuf(data) + p.message.DecodeBinary(rr) + r.Err = rr.Err +} + +// Hash implements payload.ConsensusPayload interface. func (p *Payload) Hash() util.Uint256 { w := io.NewBufBinWriter() p.EncodeBinaryUnsigned(w.BinWriter) @@ -88,6 +243,9 @@ func (p *Payload) Hash() util.Uint256 { // DecodeBinary implements io.Serializable interface. func (p *Payload) DecodeBinary(r *io.BinReader) { p.DecodeBinaryUnsigned(r) + if r.Err != nil { + return + } var b byte r.ReadLE(&b) @@ -114,8 +272,8 @@ func (m *message) DecodeBinary(r *io.BinReader) { switch m.Type { case changeViewType: cv := new(changeView) - // NewViewNumber is not marshaled - cv.NewViewNumber = m.ViewNumber + 1 + // newViewNumber is not marshaled + cv.newViewNumber = m.ViewNumber + 1 m.payload = cv case prepareRequestType: m.payload = new(prepareRequest) diff --git a/pkg/consensus/payload_test.go b/pkg/consensus/payload_test.go index 8759ead89..c6312c006 100644 --- a/pkg/consensus/payload_test.go +++ b/pkg/consensus/payload_test.go @@ -8,8 +8,11 @@ import ( "time" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/nspcc-dev/dbft/payload" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,6 +25,53 @@ var messageTypes = []messageType{ recoveryMessageType, } +func TestConsensusPayload_Setters(t *testing.T) { + var p Payload + + p.SetVersion(1) + assert.EqualValues(t, 1, p.Version()) + + p.SetPrevHash(util.Uint256{1, 2, 3}) + assert.Equal(t, util.Uint256{1, 2, 3}, p.PrevHash()) + + p.SetValidatorIndex(4) + assert.EqualValues(t, 4, p.ValidatorIndex()) + + p.SetHeight(11) + assert.EqualValues(t, 11, p.Height()) + + p.SetViewNumber(2) + assert.EqualValues(t, 2, p.ViewNumber()) + + p.SetType(payload.PrepareRequestType) + assert.Equal(t, payload.PrepareRequestType, p.Type()) + + pl := randomMessage(t, prepareRequestType) + p.SetPayload(pl) + require.Equal(t, pl, p.Payload()) + require.Equal(t, pl, p.GetPrepareRequest()) + + pl = randomMessage(t, prepareResponseType) + p.SetPayload(pl) + require.Equal(t, pl, p.GetPrepareResponse()) + + pl = randomMessage(t, commitType) + p.SetPayload(pl) + require.Equal(t, pl, p.GetCommit()) + + pl = randomMessage(t, changeViewType) + p.SetPayload(pl) + require.Equal(t, pl, p.GetChangeView()) + + pl = randomMessage(t, recoveryRequestType) + p.SetPayload(pl) + require.Equal(t, pl, p.GetRecoveryRequest()) + + pl = randomMessage(t, recoveryMessageType) + p.SetPayload(pl) + require.Equal(t, pl, p.GetRecoveryMessage()) +} + func TestConsensusPayload_Hash(t *testing.T) { dataHex := "00000000d8fb8d3b143b5f98468ef701909c976505a110a01e26c5e99be9a90cff979199b6fc33000400000000008d2000184dc95de24018f9ad71f4448a2b438eaca8b4b2ab6b4524b5a69a45d920c35103f3901444320656c390ff39c0062f5e8e138ce446a40c7e4ba1af1f8247ebbdf49295933715d3a67949714ff924f8a28cec5b954c71eca3bfaf0e9d4b1f87b4e21e9ba4ae18f97de71501b5c5d07edc200bd66a46b9b28b1a371f2195c10b0af90000e24018f900000000014140c9faaee59942f58da0e5268bc199632f2a3ad0fcbee68681a4437f140b49512e8d9efc6880eb44d3490782895a5794f35eeccee2923ce0c76fa7a1890f934eac232103c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1ac" data, err := hex.DecodeString(dataHex) @@ -40,9 +90,76 @@ func TestConsensusPayload_Serializable(t *testing.T) { for _, mt := range messageTypes { p := randomPayload(t, mt) testSerializable(t, p, new(Payload)) + + data := p.MarshalUnsigned() + pu := new(Payload) + require.NoError(t, pu.UnmarshalUnsigned(data)) + + p.Witness = transaction.Witness{} + require.Equal(t, p, pu) } } +func TestConsensusPayload_DecodeBinaryInvalid(t *testing.T) { + // PrepareResponse ConsensusPayload consists of: + // 46-byte common prefix + // 1-byte varint length of the payload (34), + // - 1-byte view number + // - 1-byte message type (PrepareResponse) + // - 32-byte preparation hash + // 1-byte delimiter (1) + // 2-byte for empty invocation and verification scripts + const ( + lenIndex = 46 + typeIndex = 47 + delimeterIndex = 81 + ) + + buf := make([]byte, 46+1+34+1+2) + + expected := &Payload{ + message: message{ + Type: prepareResponseType, + payload: &prepareResponse{}, + }, + Witness: transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: []byte{}, + }, + } + + // valid payload + buf[delimeterIndex] = 1 + buf[lenIndex] = 34 + buf[typeIndex] = byte(prepareResponseType) + r := io.NewBinReaderFromBuf(buf) + p := new(Payload) + p.DecodeBinary(r) + require.NoError(t, r.Err) + require.Equal(t, expected, p) + + // invalid type + buf[typeIndex] = 0xFF + r = io.NewBinReaderFromBuf(buf) + new(Payload).DecodeBinary(r) + require.Error(t, r.Err) + + // invalid format + buf[delimeterIndex] = 0 + buf[typeIndex] = byte(prepareResponseType) + r = io.NewBinReaderFromBuf(buf) + new(Payload).DecodeBinary(r) + require.Error(t, r.Err) + + // invalid message length + buf[delimeterIndex] = 1 + buf[lenIndex] = 0xFF + buf[typeIndex] = byte(prepareResponseType) + r = io.NewBinReaderFromBuf(buf) + new(Payload).DecodeBinary(r) + require.Error(t, r.Err) +} + func TestCommit_Serializable(t *testing.T) { c := randomMessage(t, commitType) testSerializable(t, c, new(commit)) @@ -75,19 +192,19 @@ func randomPayload(t *testing.T, mt messageType) *Payload { ViewNumber: byte(rand.Uint32()), payload: randomMessage(t, mt), }, - Version: 1, - ValidatorIndex: 13, - Height: rand.Uint32(), - Timestamp: rand.Uint32(), + version: 1, + validatorIndex: 13, + height: rand.Uint32(), + timestamp: rand.Uint32(), Witness: transaction.Witness{ InvocationScript: fillRandom(t, make([]byte, 3)), VerificationScript: fillRandom(t, make([]byte, 4)), }, } - fillRandom(t, p.PrevHash[:]) + fillRandom(t, p.prevHash[:]) if mt == changeViewType { - p.payload.(*changeView).NewViewNumber = p.ViewNumber + 1 + p.payload.(*changeView).newViewNumber = p.ViewNumber() + 1 } return p @@ -97,20 +214,20 @@ func randomMessage(t *testing.T, mt messageType) io.Serializable { switch mt { case changeViewType: return &changeView{ - Timestamp: rand.Uint32(), + timestamp: rand.Uint32(), } case prepareRequestType: return randomPrepareRequest(t) case prepareResponseType: resp := &prepareResponse{} - fillRandom(t, resp.PreparationHash[:]) + fillRandom(t, resp.preparationHash[:]) return resp case commitType: var c commit - fillRandom(t, c.Signature[:]) + fillRandom(t, c.signature[:]) return &c case recoveryRequestType: - return &recoveryRequest{Timestamp: rand.Uint32()} + return &recoveryRequest{timestamp: rand.Uint32()} case recoveryMessageType: return randomRecoveryMessage(t) default: @@ -123,17 +240,17 @@ func randomPrepareRequest(t *testing.T) *prepareRequest { const txCount = 3 req := &prepareRequest{ - Timestamp: rand.Uint32(), - Nonce: rand.Uint64(), - TransactionHashes: make([]util.Uint256, txCount), - MinerTransaction: *newMinerTx(rand.Uint32()), + timestamp: rand.Uint32(), + nonce: rand.Uint64(), + transactionHashes: make([]util.Uint256, txCount), + minerTx: *newMinerTx(rand.Uint32()), } - req.TransactionHashes[0] = req.MinerTransaction.Hash() + req.transactionHashes[0] = req.minerTx.Hash() for i := 1; i < txCount; i++ { - fillRandom(t, req.TransactionHashes[i][:]) + fillRandom(t, req.transactionHashes[i][:]) } - fillRandom(t, req.NextConsensus[:]) + fillRandom(t, req.nextConsensus[:]) return req } @@ -144,27 +261,27 @@ func randomRecoveryMessage(t *testing.T) *recoveryMessage { prepReq := result.(*prepareRequest) return &recoveryMessage{ - PreparationPayloads: []*preparationCompact{ + preparationPayloads: []*preparationCompact{ { ValidatorIndex: 1, InvocationScript: fillRandom(t, make([]byte, 10)), }, }, - CommitPayloads: []*commitCompact{ + commitPayloads: []*commitCompact{ { ViewNumber: 0, ValidatorIndex: 1, - Signature: fillRandom(t, make([]byte, signatureSize)), + Signature: [64]byte{1, 2, 3}, InvocationScript: fillRandom(t, make([]byte, 20)), }, { ViewNumber: 0, ValidatorIndex: 2, - Signature: fillRandom(t, make([]byte, signatureSize)), + Signature: [64]byte{11, 3, 4, 98}, InvocationScript: fillRandom(t, make([]byte, 10)), }, }, - ChangeViewPayloads: []*changeViewCompact{ + changeViewPayloads: []*changeViewCompact{ { Timestamp: rand.Uint32(), ValidatorIndex: 3, @@ -172,10 +289,21 @@ func randomRecoveryMessage(t *testing.T) *recoveryMessage { InvocationScript: fillRandom(t, make([]byte, 4)), }, }, - PrepareRequest: prepReq, + prepareRequest: prepReq, } } +func TestPayload_Sign(t *testing.T) { + key, err := keys.NewPrivateKey() + require.NoError(t, err) + + priv := &privateKey{key} + p := randomPayload(t, prepareRequestType) + require.False(t, p.Verify()) + require.NoError(t, p.Sign(priv)) + require.True(t, p.Verify()) +} + func TestMessageType_String(t *testing.T) { require.Equal(t, "ChangeView", changeViewType.String()) require.Equal(t, "PrepareRequest", prepareRequestType.String()) diff --git a/pkg/consensus/prepare_request.go b/pkg/consensus/prepare_request.go index 8f6f500cd..6966de0f5 100644 --- a/pkg/consensus/prepare_request.go +++ b/pkg/consensus/prepare_request.go @@ -4,31 +4,58 @@ import ( "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/nspcc-dev/dbft/payload" ) -// prepareRequest represents dBFT PrepareRequest message. +// prepareRequest represents dBFT prepareRequest message. type prepareRequest struct { - Timestamp uint32 - Nonce uint64 - TransactionHashes []util.Uint256 - MinerTransaction transaction.Transaction - NextConsensus util.Uint160 + timestamp uint32 + nonce uint64 + transactionHashes []util.Uint256 + minerTx transaction.Transaction + nextConsensus util.Uint160 } +var _ payload.PrepareRequest = (*prepareRequest)(nil) + // EncodeBinary implements io.Serializable interface. func (p *prepareRequest) EncodeBinary(w *io.BinWriter) { - w.WriteLE(p.Timestamp) - w.WriteLE(p.Nonce) - w.WriteBE(p.NextConsensus[:]) - w.WriteArray(p.TransactionHashes) - p.MinerTransaction.EncodeBinary(w) + w.WriteLE(p.timestamp) + w.WriteLE(p.nonce) + w.WriteBE(p.nextConsensus[:]) + w.WriteArray(p.transactionHashes) + p.minerTx.EncodeBinary(w) } // DecodeBinary implements io.Serializable interface. func (p *prepareRequest) DecodeBinary(r *io.BinReader) { - r.ReadLE(&p.Timestamp) - r.ReadLE(&p.Nonce) - r.ReadBE(p.NextConsensus[:]) - r.ReadArray(&p.TransactionHashes) - p.MinerTransaction.DecodeBinary(r) + r.ReadLE(&p.timestamp) + r.ReadLE(&p.nonce) + r.ReadBE(p.nextConsensus[:]) + r.ReadArray(&p.transactionHashes) + p.minerTx.DecodeBinary(r) } + +// Timestamp implements payload.PrepareRequest interface. +func (p *prepareRequest) Timestamp() uint32 { return p.timestamp } + +// SetTimestamp implements payload.PrepareRequest interface. +func (p *prepareRequest) SetTimestamp(ts uint32) { p.timestamp = ts } + +// Nonce implements payload.PrepareRequest interface. +func (p *prepareRequest) Nonce() uint64 { return p.nonce } + +// SetNonce implements payload.PrepareRequest interface. +func (p *prepareRequest) SetNonce(nonce uint64) { p.nonce = nonce } + +// TransactionHashes implements payload.PrepareRequest interface. +func (p *prepareRequest) TransactionHashes() []util.Uint256 { return p.transactionHashes } + +// SetTransactionHashes implements payload.PrepareRequest interface. +func (p *prepareRequest) SetTransactionHashes(hs []util.Uint256) { p.transactionHashes = hs } + +// NextConsensus implements payload.PrepareRequest interface. +func (p *prepareRequest) NextConsensus() util.Uint160 { return p.nextConsensus } + +// SetNextConsensus implements payload.PrepareRequest interface. +func (p *prepareRequest) SetNextConsensus(nc util.Uint160) { p.nextConsensus = nc } diff --git a/pkg/consensus/prepare_request_test.go b/pkg/consensus/prepare_request_test.go new file mode 100644 index 000000000..83b80c3f5 --- /dev/null +++ b/pkg/consensus/prepare_request_test.go @@ -0,0 +1,28 @@ +package consensus + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestPrepareRequest_Setters(t *testing.T) { + var p prepareRequest + + p.SetTimestamp(123) + require.EqualValues(t, 123, p.Timestamp()) + + p.SetNextConsensus(util.Uint160{5, 6, 7}) + require.Equal(t, util.Uint160{5, 6, 7}, p.NextConsensus()) + + p.SetNonce(8765) + require.EqualValues(t, 8765, p.Nonce()) + + var hashes [2]util.Uint256 + fillRandom(t, hashes[0][:]) + fillRandom(t, hashes[1][:]) + + p.SetTransactionHashes(hashes[:]) + require.Equal(t, hashes[:], p.TransactionHashes()) +} diff --git a/pkg/consensus/prepare_response.go b/pkg/consensus/prepare_response.go index 4636b3139..8d338bc62 100644 --- a/pkg/consensus/prepare_response.go +++ b/pkg/consensus/prepare_response.go @@ -3,19 +3,28 @@ package consensus import ( "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/nspcc-dev/dbft/payload" ) // prepareResponse represents dBFT PrepareResponse message. type prepareResponse struct { - PreparationHash util.Uint256 + preparationHash util.Uint256 } +var _ payload.PrepareResponse = (*prepareResponse)(nil) + // EncodeBinary implements io.Serializable interface. func (p *prepareResponse) EncodeBinary(w *io.BinWriter) { - w.WriteBE(p.PreparationHash[:]) + w.WriteBE(p.preparationHash[:]) } // DecodeBinary implements io.Serializable interface. func (p *prepareResponse) DecodeBinary(r *io.BinReader) { - r.ReadBE(p.PreparationHash[:]) + r.ReadBE(p.preparationHash[:]) } + +// PreparationHash implements payload.PrepareResponse interface. +func (p *prepareResponse) PreparationHash() util.Uint256 { return p.preparationHash } + +// SetPreparationHash implements payload.PrepareResponse interface. +func (p *prepareResponse) SetPreparationHash(h util.Uint256) { p.preparationHash = h } diff --git a/pkg/consensus/prepare_response_test.go b/pkg/consensus/prepare_response_test.go new file mode 100644 index 000000000..171a128ea --- /dev/null +++ b/pkg/consensus/prepare_response_test.go @@ -0,0 +1,15 @@ +package consensus + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestPrepareResponse_Setters(t *testing.T) { + var p prepareResponse + + p.SetPreparationHash(util.Uint256{1, 2, 3}) + require.Equal(t, util.Uint256{1, 2, 3}, p.PreparationHash()) +} diff --git a/pkg/consensus/recovery_message.go b/pkg/consensus/recovery_message.go index e1dddc27e..8e0d62533 100644 --- a/pkg/consensus/recovery_message.go +++ b/pkg/consensus/recovery_message.go @@ -3,17 +3,18 @@ package consensus import ( "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/nspcc-dev/dbft/payload" "github.com/pkg/errors" ) type ( // recoveryMessage represents dBFT Recovery message. recoveryMessage struct { - PreparationHash *util.Uint256 - PreparationPayloads []*preparationCompact - CommitPayloads []*commitCompact - ChangeViewPayloads []*changeViewCompact - PrepareRequest *prepareRequest + preparationHash *util.Uint256 + preparationPayloads []*preparationCompact + commitPayloads []*commitCompact + changeViewPayloads []*changeViewCompact + prepareRequest *prepareRequest } changeViewCompact struct { @@ -26,7 +27,7 @@ type ( commitCompact struct { ViewNumber byte ValidatorIndex uint16 - Signature []byte + Signature [signatureSize]byte InvocationScript []byte } @@ -36,53 +37,54 @@ type ( } ) -const uint256size = 32 +var _ payload.RecoveryMessage = (*recoveryMessage)(nil) // DecodeBinary implements io.Serializable interface. func (m *recoveryMessage) DecodeBinary(r *io.BinReader) { - r.ReadArray(&m.ChangeViewPayloads) + r.ReadArray(&m.changeViewPayloads) var hasReq bool r.ReadLE(&hasReq) if hasReq { - m.PrepareRequest = new(prepareRequest) - m.PrepareRequest.DecodeBinary(r) + m.prepareRequest = new(prepareRequest) + m.prepareRequest.DecodeBinary(r) } else { l := r.ReadVarUint() if l != 0 { - if l == uint256size { - m.PreparationHash = new(util.Uint256) - r.ReadBE(m.PreparationHash[:]) + if l == util.Uint256Size { + m.preparationHash = new(util.Uint256) + r.ReadBE(m.preparationHash[:]) } else { r.Err = errors.New("invalid data") } } else { - m.PreparationHash = nil + m.preparationHash = nil } } - r.ReadArray(&m.PreparationPayloads) - r.ReadArray(&m.CommitPayloads) + r.ReadArray(&m.preparationPayloads) + r.ReadArray(&m.commitPayloads) } // EncodeBinary implements io.Serializable interface. func (m *recoveryMessage) EncodeBinary(w *io.BinWriter) { - w.WriteArray(m.ChangeViewPayloads) + w.WriteArray(m.changeViewPayloads) - hasReq := m.PrepareRequest != nil + hasReq := m.prepareRequest != nil w.WriteLE(hasReq) if hasReq { - m.PrepareRequest.EncodeBinary(w) + m.prepareRequest.EncodeBinary(w) } else { - if m.PreparationHash == nil { + if m.preparationHash == nil { w.WriteVarUint(0) } else { - w.WriteVarUint(uint256size) - w.WriteBE(m.PreparationHash[:]) + w.WriteVarUint(util.Uint256Size) + w.WriteBE(m.preparationHash[:]) } } - w.WriteArray(m.PreparationPayloads) - w.WriteArray(m.CommitPayloads) + + w.WriteArray(m.preparationPayloads) + w.WriteArray(m.commitPayloads) } // DecodeBinary implements io.Serializable interface. @@ -105,9 +107,7 @@ func (p *changeViewCompact) EncodeBinary(w *io.BinWriter) { func (p *commitCompact) DecodeBinary(r *io.BinReader) { r.ReadLE(&p.ViewNumber) r.ReadLE(&p.ValidatorIndex) - - p.Signature = make([]byte, signatureSize) - r.ReadBE(&p.Signature) + r.ReadBE(p.Signature[:]) p.InvocationScript = r.ReadBytes() } @@ -130,3 +130,108 @@ func (p *preparationCompact) EncodeBinary(w *io.BinWriter) { w.WriteLE(p.ValidatorIndex) w.WriteBytes(p.InvocationScript) } + +// AddPayload implements payload.RecoveryMessage interface. +func (m *recoveryMessage) AddPayload(p payload.ConsensusPayload) { + switch p.Type() { + case payload.PrepareRequestType: + m.prepareRequest = p.GetPrepareRequest().(*prepareRequest) + case payload.PrepareResponseType: + m.preparationPayloads = append(m.preparationPayloads, &preparationCompact{ + ValidatorIndex: p.ValidatorIndex(), + InvocationScript: p.(*Payload).Witness.InvocationScript, + }) + case payload.ChangeViewType: + m.changeViewPayloads = append(m.changeViewPayloads, &changeViewCompact{ + ValidatorIndex: p.ValidatorIndex(), + OriginalViewNumber: p.ViewNumber(), + Timestamp: p.GetChangeView().Timestamp(), + InvocationScript: p.(*Payload).Witness.InvocationScript, + }) + case payload.CommitType: + m.commitPayloads = append(m.commitPayloads, &commitCompact{ + ValidatorIndex: p.ValidatorIndex(), + ViewNumber: p.ViewNumber(), + Signature: p.GetCommit().(*commit).signature, + InvocationScript: p.(*Payload).Witness.InvocationScript, + }) + } +} + +// GetPrepareRequest implements payload.RecoveryMessage interface. +func (m *recoveryMessage) GetPrepareRequest(p payload.ConsensusPayload) payload.ConsensusPayload { + if m.prepareRequest == nil { + return nil + } + + return fromPayload(prepareRequestType, p.(*Payload), m.prepareRequest) +} + +// GetPrepareResponses implements payload.RecoveryMessage interface. +func (m *recoveryMessage) GetPrepareResponses(p payload.ConsensusPayload) []payload.ConsensusPayload { + if m.preparationHash == nil { + return nil + } + + ps := make([]payload.ConsensusPayload, len(m.preparationPayloads)) + + for i, resp := range m.preparationPayloads { + ps[i] = fromPayload(prepareResponseType, p.(*Payload), &prepareResponse{ + preparationHash: *m.preparationHash, + }) + ps[i].SetValidatorIndex(resp.ValidatorIndex) + } + + return ps +} + +// GetChangeViews implements payload.RecoveryMessage interface. +func (m *recoveryMessage) GetChangeViews(p payload.ConsensusPayload) []payload.ConsensusPayload { + ps := make([]payload.ConsensusPayload, len(m.changeViewPayloads)) + + for i, cv := range m.changeViewPayloads { + ps[i] = fromPayload(changeViewType, p.(*Payload), &changeView{ + newViewNumber: cv.OriginalViewNumber + 1, + timestamp: cv.Timestamp, + }) + ps[i].SetValidatorIndex(cv.ValidatorIndex) + } + + return ps +} + +// GetCommits implements payload.RecoveryMessage interface. +func (m *recoveryMessage) GetCommits(p payload.ConsensusPayload) []payload.ConsensusPayload { + ps := make([]payload.ConsensusPayload, len(m.commitPayloads)) + + for i, c := range m.commitPayloads { + cc := commit{signature: c.Signature} + ps[i] = fromPayload(commitType, p.(*Payload), &cc) + ps[i].SetValidatorIndex(c.ValidatorIndex) + } + + return ps +} + +// PreparationHash implements payload.RecoveryMessage interface. +func (m *recoveryMessage) PreparationHash() *util.Uint256 { + return m.preparationHash +} + +// SetPreparationHash implements payload.RecoveryMessage interface. +func (m *recoveryMessage) SetPreparationHash(h *util.Uint256) { + m.preparationHash = h +} + +func fromPayload(t messageType, recovery *Payload, p io.Serializable) *Payload { + return &Payload{ + message: message{ + Type: t, + ViewNumber: recovery.message.ViewNumber, + payload: p, + }, + version: recovery.Version(), + prevHash: recovery.PrevHash(), + height: recovery.Height(), + } +} diff --git a/pkg/consensus/recovery_request.go b/pkg/consensus/recovery_request.go index 72c74b9ee..d73f34c08 100644 --- a/pkg/consensus/recovery_request.go +++ b/pkg/consensus/recovery_request.go @@ -1,18 +1,29 @@ package consensus -import "github.com/CityOfZion/neo-go/pkg/io" +import ( + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/nspcc-dev/dbft/payload" +) // recoveryRequest represents dBFT RecoveryRequest message. type recoveryRequest struct { - Timestamp uint32 + timestamp uint32 } +var _ payload.RecoveryRequest = (*recoveryRequest)(nil) + // DecodeBinary implements io.Serializable interface. func (m *recoveryRequest) DecodeBinary(r *io.BinReader) { - r.ReadLE(&m.Timestamp) + r.ReadLE(&m.timestamp) } // EncodeBinary implements io.Serializable interface. func (m *recoveryRequest) EncodeBinary(w *io.BinWriter) { - w.WriteLE(m.Timestamp) + w.WriteLE(m.timestamp) } + +// Timestamp implements payload.RecoveryRequest interface. +func (m *recoveryRequest) Timestamp() uint32 { return m.timestamp } + +// SetTimestamp implements payload.RecoveryRequest interface. +func (m *recoveryRequest) SetTimestamp(ts uint32) { m.timestamp = ts } diff --git a/pkg/consensus/recovery_request_test.go b/pkg/consensus/recovery_request_test.go new file mode 100644 index 000000000..e061711ff --- /dev/null +++ b/pkg/consensus/recovery_request_test.go @@ -0,0 +1,14 @@ +package consensus + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRecoveryRequest_Setters(t *testing.T) { + var r recoveryRequest + + r.SetTimestamp(123) + require.EqualValues(t, 123, r.Timestamp()) +} diff --git a/pkg/core/block_base.go b/pkg/core/block_base.go index 48a4e8d08..dfbc6cd15 100644 --- a/pkg/core/block_base.go +++ b/pkg/core/block_base.go @@ -91,8 +91,8 @@ func (b *BlockBase) EncodeBinary(bw *io.BinWriter) { b.Script.EncodeBinary(bw) } -// getHashableData returns serialized hashable data of the block. -func (b *BlockBase) getHashableData() []byte { +// GetHashableData returns serialized hashable data of the block. +func (b *BlockBase) GetHashableData() []byte { buf := io.NewBufBinWriter() // No error can occure while encoding hashable fields. b.encodeHashableFields(buf.BinWriter) @@ -107,7 +107,7 @@ func (b *BlockBase) getHashableData() []byte { // Since MerkleRoot already contains the hash value of all transactions, // the modification of transaction will influence the hash value of the block. func (b *BlockBase) createHash() { - bb := b.getHashableData() + bb := b.GetHashableData() b.hash = hash.DoubleSha256(bb) b.verificationHash = hash.Sha256(bb) } diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index dd74f78cd..c9aa54416 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -79,7 +79,7 @@ func newBlock(index uint32, txs ...*transaction.Transaction) *Block { if err != nil { panic(err) } - b := b.getHashableData() + b := b.GetHashableData() sig, err := pKey.Sign(b) if err != nil || len(sig) != 64 { panic(err) diff --git a/pkg/network/server.go b/pkg/network/server.go index edffdc0cc..a7d5765b6 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -16,6 +16,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/network/payload" "github.com/CityOfZion/neo-go/pkg/util" log "github.com/sirupsen/logrus" + "go.uber.org/atomic" ) const ( @@ -61,6 +62,8 @@ type ( register chan Peer unregister chan peerDrop quit chan struct{} + + connected *atomic.Bool } peerDrop struct { @@ -87,9 +90,21 @@ func NewServer(config ServerConfig, chain core.Blockchainer) *Server { register: make(chan Peer), unregister: make(chan peerDrop), peers: make(map[Peer]bool), - consensus: consensus.NewService(), + connected: atomic.NewBool(false), } + srv, err := consensus.NewService(consensus.Config{ + Broadcast: s.handleNewPayload, + Chain: chain, + RequestTx: s.requestTx, + Wallet: config.Wallet, + }) + if err != nil { + return nil + } + + s.consensus = srv + if s.MinPeers <= 0 { log.WithFields(log.Fields{ "MinPeers configured": s.MinPeers, @@ -233,10 +248,31 @@ func (s *Server) run() { } } +func (s *Server) tryStartConsensus() { + if s.Wallet == nil || s.connected.Load() { + return + } + + if s.PeerCount() >= s.MinPeers { + log.Info("minimum amount of peers were connected to") + if s.connected.CAS(false, true) { + s.consensus.Start() + } + } +} + // Peers returns the current list of peers connected to // the server. func (s *Server) Peers() map[Peer]bool { - return s.peers + s.lock.RLock() + defer s.lock.RUnlock() + + peers := make(map[Peer]bool, len(s.peers)) + for k, v := range s.peers { + peers[k] = v + } + + return peers } // PeerCount returns the number of current connected peers. @@ -394,6 +430,13 @@ func (s *Server) handleConsensusCmd(cp *consensus.Payload) error { return nil } +// handleTxCmd processes received transaction. +// It never returns an error. +func (s *Server) handleTxCmd(tx *transaction.Transaction) error { + s.consensus.OnTransaction(tx) + return nil +} + // handleAddrCmd will process received addresses. func (s *Server) handleAddrCmd(p Peer, addrs *payload.AddressList) error { for _, a := range addrs.Addrs { @@ -485,6 +528,9 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { case CMDConsensus: cp := msg.Payload.(*consensus.Payload) return s.handleConsensusCmd(cp) + case CMDTX: + tx := msg.Payload.(*transaction.Transaction) + return s.handleTxCmd(tx) case CMDVersion, CMDVerack: return fmt.Errorf("received '%s' after the handshake", msg.CommandType()) } @@ -499,6 +545,8 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { return err } go s.startProtocol(peer) + + s.tryStartConsensus() default: return fmt.Errorf("received '%s' during handshake", msg.CommandType()) } @@ -506,6 +554,28 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { return nil } +func (s *Server) handleNewPayload(p *consensus.Payload) { + s.relayInventory(payload.ConsensusType, p.Hash()) +} + +func (s *Server) requestTx(hashes ...util.Uint256) { + if len(hashes) == 0 { + return + } + + s.relayInventory(payload.TXType, hashes...) +} + +func (s *Server) relayInventory(t payload.InventoryType, hashes ...util.Uint256) { + for peer := range s.Peers() { + if !peer.Handshaked() { + continue + } + payload := payload.NewInventory(t, hashes) + s.RelayDirectly(peer, payload) + } +} + // RelayTxn a new transaction to the local node and the connected peers. // Reference: the method OnRelay in C#: https://github.com/neo-project/neo/blob/master/neo/Network/P2P/LocalNode.cs#L159 func (s *Server) RelayTxn(t *transaction.Transaction) RelayReason { diff --git a/pkg/network/server_config.go b/pkg/network/server_config.go index f101ba27c..b51daf0e1 100644 --- a/pkg/network/server_config.go +++ b/pkg/network/server_config.go @@ -54,6 +54,9 @@ type ( // Level of the internal logger. LogLevel log.Level + + // Wallet is a wallet configuration. + Wallet *config.WalletConfig } ) @@ -63,6 +66,11 @@ func NewServerConfig(cfg config.Config) ServerConfig { appConfig := cfg.ApplicationConfiguration protoConfig := cfg.ProtocolConfiguration + var wc *config.WalletConfig + if appConfig.UnlockWallet.Path != "" { + wc = &appConfig.UnlockWallet + } + return ServerConfig{ UserAgent: cfg.GenerateUserAgent(), Address: appConfig.Address, @@ -75,5 +83,6 @@ func NewServerConfig(cfg config.Config) ServerConfig { MaxPeers: appConfig.MaxPeers, AttemptConnPeers: appConfig.AttemptConnPeers, MinPeers: appConfig.MinPeers, + Wallet: wc, } } diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 3a7b63e94..02fb52b75 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -81,6 +81,11 @@ func (a *Account) Encrypt(passphrase string) error { return nil } +// PrivateKey returns private key corresponding to the account. +func (a *Account) PrivateKey() *keys.PrivateKey { + return a.privateKey +} + // NewAccountFromWIF creates a new Account from the given WIF. func NewAccountFromWIF(wif string) (*Account, error) { privKey, err := keys.NewPrivateKeyFromWIF(wif)