mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-04 19:19:44 +00:00
consensus: move stateroot message generation to commit phase
* update dbft library, change to 64-bit timestamps and CV reason * modify messages * generate stateroot witness data from prepare* messages during commit Fix #1273.
This commit is contained in:
parent
92851aa8e4
commit
253c39d4ee
14 changed files with 172 additions and 119 deletions
2
go.mod
2
go.mod
|
@ -9,7 +9,7 @@ require (
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible
|
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/mr-tron/base58 v1.1.2
|
github.com/mr-tron/base58 v1.1.2
|
||||||
github.com/nspcc-dev/dbft v0.0.0-20200623100921-5a182c20965e
|
github.com/nspcc-dev/dbft v0.0.0-20200810081309-f40804dbf8a0
|
||||||
github.com/nspcc-dev/rfc6979 v0.2.0
|
github.com/nspcc-dev/rfc6979 v0.2.0
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/prometheus/client_golang v1.2.1
|
github.com/prometheus/client_golang v1.2.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -156,8 +156,8 @@ github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a h1:ajvxgEe9qY4vvoSm
|
||||||
github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a/go.mod h1:/YFK+XOxxg0Bfm6P92lY5eDSLYfp06XOdL8KAVgXjVk=
|
github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a/go.mod h1:/YFK+XOxxg0Bfm6P92lY5eDSLYfp06XOdL8KAVgXjVk=
|
||||||
github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1 h1:yEx9WznS+rjE0jl0dLujCxuZSIb+UTjF+005TJu/nNI=
|
github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1 h1:yEx9WznS+rjE0jl0dLujCxuZSIb+UTjF+005TJu/nNI=
|
||||||
github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1/go.mod h1:O0qtn62prQSqizzoagHmuuKoz8QMkU3SzBoKdEvm3aQ=
|
github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1/go.mod h1:O0qtn62prQSqizzoagHmuuKoz8QMkU3SzBoKdEvm3aQ=
|
||||||
github.com/nspcc-dev/dbft v0.0.0-20200623100921-5a182c20965e h1:QOT9slflIkEKb5wY0ZUC0dCmCgoqGlhOAh9+xWMIxfg=
|
github.com/nspcc-dev/dbft v0.0.0-20200810081309-f40804dbf8a0 h1:4XrUJSvClcBQVZJQqI9EHW/kAIWcrycgTa5J0lBO3R8=
|
||||||
github.com/nspcc-dev/dbft v0.0.0-20200623100921-5a182c20965e/go.mod h1:1FYQXSbb6/9HQIkoF8XO7W/S8N7AZRkBsgwbcXRvk0E=
|
github.com/nspcc-dev/dbft v0.0.0-20200810081309-f40804dbf8a0/go.mod h1:1FYQXSbb6/9HQIkoF8XO7W/S8N7AZRkBsgwbcXRvk0E=
|
||||||
github.com/nspcc-dev/neo-go v0.73.1-pre.0.20200303142215-f5a1b928ce09/go.mod h1:pPYwPZ2ks+uMnlRLUyXOpLieaDQSEaf4NM3zHVbRjmg=
|
github.com/nspcc-dev/neo-go v0.73.1-pre.0.20200303142215-f5a1b928ce09/go.mod h1:pPYwPZ2ks+uMnlRLUyXOpLieaDQSEaf4NM3zHVbRjmg=
|
||||||
github.com/nspcc-dev/neofs-crypto v0.2.0 h1:ftN+59WqxSWz/RCgXYOfhmltOOqU+udsNQSvN6wkFck=
|
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/neofs-crypto v0.2.0/go.mod h1:F/96fUzPM3wR+UGsPi3faVNmFlA9KAEAUQR7dMxZmNA=
|
||||||
|
|
|
@ -30,7 +30,13 @@ func (c changeView) NewViewNumber() byte { return c.newViewNumber }
|
||||||
func (c *changeView) SetNewViewNumber(view byte) { c.newViewNumber = view }
|
func (c *changeView) SetNewViewNumber(view byte) { c.newViewNumber = view }
|
||||||
|
|
||||||
// Timestamp implements payload.ChangeView interface.
|
// Timestamp implements payload.ChangeView interface.
|
||||||
func (c changeView) Timestamp() uint32 { return c.timestamp }
|
func (c changeView) Timestamp() uint64 { return uint64(c.timestamp) * nanoInSec }
|
||||||
|
|
||||||
// SetTimestamp implements payload.ChangeView interface.
|
// SetTimestamp implements payload.ChangeView interface.
|
||||||
func (c *changeView) SetTimestamp(ts uint32) { c.timestamp = ts }
|
func (c *changeView) SetTimestamp(ts uint64) { c.timestamp = uint32(ts / nanoInSec) }
|
||||||
|
|
||||||
|
// Reason implements payload.ChangeView interface.
|
||||||
|
func (c changeView) Reason() payload.ChangeViewReason { return payload.CVUnknown }
|
||||||
|
|
||||||
|
// SetReason implements payload.ChangeView interface.
|
||||||
|
func (c *changeView) SetReason(_ payload.ChangeViewReason) {}
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
func TestChangeView_Setters(t *testing.T) {
|
func TestChangeView_Setters(t *testing.T) {
|
||||||
var c changeView
|
var c changeView
|
||||||
|
|
||||||
c.SetTimestamp(123)
|
c.SetTimestamp(123 * nanoInSec)
|
||||||
require.EqualValues(t, 123, c.Timestamp())
|
require.EqualValues(t, 123*nanoInSec, c.Timestamp())
|
||||||
|
|
||||||
c.SetNewViewNumber(2)
|
c.SetNewViewNumber(2)
|
||||||
require.EqualValues(t, 2, c.NewViewNumber())
|
require.EqualValues(t, 2, c.NewViewNumber())
|
||||||
|
|
|
@ -8,9 +8,6 @@ import (
|
||||||
// commit represents dBFT Commit message.
|
// commit represents dBFT Commit message.
|
||||||
type commit struct {
|
type commit struct {
|
||||||
signature [signatureSize]byte
|
signature [signatureSize]byte
|
||||||
stateSig [signatureSize]byte
|
|
||||||
|
|
||||||
stateRootEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// signatureSize is an rfc6989 signature size in bytes
|
// signatureSize is an rfc6989 signature size in bytes
|
||||||
|
@ -22,17 +19,11 @@ var _ payload.Commit = (*commit)(nil)
|
||||||
// EncodeBinary implements io.Serializable interface.
|
// EncodeBinary implements io.Serializable interface.
|
||||||
func (c *commit) EncodeBinary(w *io.BinWriter) {
|
func (c *commit) EncodeBinary(w *io.BinWriter) {
|
||||||
w.WriteBytes(c.signature[:])
|
w.WriteBytes(c.signature[:])
|
||||||
if c.stateRootEnabled {
|
|
||||||
w.WriteBytes(c.stateSig[:])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeBinary implements io.Serializable interface.
|
// DecodeBinary implements io.Serializable interface.
|
||||||
func (c *commit) DecodeBinary(r *io.BinReader) {
|
func (c *commit) DecodeBinary(r *io.BinReader) {
|
||||||
r.ReadBytes(c.signature[:])
|
r.ReadBytes(c.signature[:])
|
||||||
if c.stateRootEnabled {
|
|
||||||
r.ReadBytes(c.stateSig[:])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature implements payload.Commit interface.
|
// Signature implements payload.Commit interface.
|
||||||
|
|
|
@ -16,7 +16,6 @@ import (
|
||||||
coreb "github.com/nspcc-dev/neo-go/pkg/core/block"
|
coreb "github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/cache"
|
"github.com/nspcc-dev/neo-go/pkg/core/cache"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
@ -136,7 +135,7 @@ func NewService(cfg Config) (Service, error) {
|
||||||
dbft.WithVerifyBlock(srv.verifyBlock),
|
dbft.WithVerifyBlock(srv.verifyBlock),
|
||||||
dbft.WithGetBlock(srv.getBlock),
|
dbft.WithGetBlock(srv.getBlock),
|
||||||
dbft.WithWatchOnly(func() bool { return false }),
|
dbft.WithWatchOnly(func() bool { return false }),
|
||||||
dbft.WithNewBlockFromContext(newBlockFromContext),
|
dbft.WithNewBlockFromContext(srv.newBlockFromContext),
|
||||||
dbft.WithCurrentHeight(cfg.Chain.BlockHeight),
|
dbft.WithCurrentHeight(cfg.Chain.BlockHeight),
|
||||||
dbft.WithCurrentBlockHash(cfg.Chain.CurrentBlockHash),
|
dbft.WithCurrentBlockHash(cfg.Chain.CurrentBlockHash),
|
||||||
dbft.WithGetValidators(srv.getValidators),
|
dbft.WithGetValidators(srv.getValidators),
|
||||||
|
@ -144,12 +143,13 @@ func NewService(cfg Config) (Service, error) {
|
||||||
|
|
||||||
dbft.WithNewConsensusPayload(srv.newPayload),
|
dbft.WithNewConsensusPayload(srv.newPayload),
|
||||||
dbft.WithNewPrepareRequest(srv.newPrepareRequest),
|
dbft.WithNewPrepareRequest(srv.newPrepareRequest),
|
||||||
dbft.WithNewPrepareResponse(func() payload.PrepareResponse { return new(prepareResponse) }),
|
dbft.WithNewPrepareResponse(srv.newPrepareResponse),
|
||||||
dbft.WithNewChangeView(func() payload.ChangeView { return new(changeView) }),
|
dbft.WithNewChangeView(func() payload.ChangeView { return new(changeView) }),
|
||||||
dbft.WithNewCommit(srv.newCommit),
|
dbft.WithNewCommit(func() payload.Commit { return new(commit) }),
|
||||||
dbft.WithNewRecoveryRequest(func() payload.RecoveryRequest { return new(recoveryRequest) }),
|
dbft.WithNewRecoveryRequest(func() payload.RecoveryRequest { return new(recoveryRequest) }),
|
||||||
dbft.WithNewRecoveryMessage(srv.newRecoveryMessage),
|
dbft.WithNewRecoveryMessage(srv.newRecoveryMessage),
|
||||||
dbft.WithVerifyPrepareRequest(srv.verifyRequest),
|
dbft.WithVerifyPrepareRequest(srv.verifyRequest),
|
||||||
|
dbft.WithVerifyPrepareResponse(srv.verifyResponse),
|
||||||
)
|
)
|
||||||
|
|
||||||
if srv.dbft == nil {
|
if srv.dbft == nil {
|
||||||
|
@ -235,36 +235,42 @@ func (s *service) stateRootEnabled() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) newPrepareRequest() payload.PrepareRequest {
|
func (s *service) newPrepareRequest() payload.PrepareRequest {
|
||||||
|
res := &prepareRequest{
|
||||||
|
stateRootEnabled: s.stateRootEnabled(),
|
||||||
|
}
|
||||||
if !s.stateRootEnabled() {
|
if !s.stateRootEnabled() {
|
||||||
return new(prepareRequest)
|
return res
|
||||||
}
|
}
|
||||||
sr, err := s.Chain.GetStateRoot(s.Chain.BlockHeight())
|
sig := s.getStateRootSig()
|
||||||
if err == nil {
|
if sig != nil {
|
||||||
return &prepareRequest{
|
copy(res.stateRootSig[:], sig)
|
||||||
stateRootEnabled: true,
|
|
||||||
proposalStateRoot: sr.MPTRootBase,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return &prepareRequest{stateRootEnabled: true}
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) newCommit() payload.Commit {
|
func (s *service) getStateRootSig() []byte {
|
||||||
|
var sig []byte
|
||||||
|
|
||||||
|
sr, err := s.Chain.GetStateRoot(s.dbft.BlockIndex - 1)
|
||||||
|
if err == nil {
|
||||||
|
data := sr.GetSignedPart()
|
||||||
|
sig, _ = s.dbft.Priv.Sign(data)
|
||||||
|
}
|
||||||
|
return sig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) newPrepareResponse() payload.PrepareResponse {
|
||||||
|
res := &prepareResponse{
|
||||||
|
stateRootEnabled: s.stateRootEnabled(),
|
||||||
|
}
|
||||||
if !s.stateRootEnabled() {
|
if !s.stateRootEnabled() {
|
||||||
return new(commit)
|
return res
|
||||||
}
|
}
|
||||||
c := &commit{stateRootEnabled: true}
|
sig := s.getStateRootSig()
|
||||||
for _, p := range s.dbft.Context.PreparationPayloads {
|
if sig != nil {
|
||||||
if p != nil && p.ViewNumber() == s.dbft.ViewNumber && p.Type() == payload.PrepareRequestType {
|
copy(res.stateRootSig[:], sig)
|
||||||
pr := p.GetPrepareRequest().(*prepareRequest)
|
|
||||||
data := pr.proposalStateRoot.GetSignedPart()
|
|
||||||
sign, err := s.dbft.Priv.Sign(data)
|
|
||||||
if err == nil {
|
|
||||||
copy(c.stateSig[:], sign)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return c
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) newRecoveryMessage() payload.RecoveryMessage {
|
func (s *service) newRecoveryMessage() payload.RecoveryMessage {
|
||||||
|
@ -393,15 +399,29 @@ func (s *service) verifyBlock(b block.Block) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) verifyStateRootSig(index int, sig []byte) error {
|
||||||
|
r, err := s.Chain.GetStateRoot(s.dbft.BlockIndex - 1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get local state root: %v", err)
|
||||||
|
}
|
||||||
|
validators := s.getValidators()
|
||||||
|
if index >= len(validators) {
|
||||||
|
return errors.New("bad validator index")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub := validators[index]
|
||||||
|
if pub.Verify(r.GetSignedPart(), sig) != nil {
|
||||||
|
return errors.New("bad state root signature")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) verifyRequest(p payload.ConsensusPayload) error {
|
func (s *service) verifyRequest(p payload.ConsensusPayload) error {
|
||||||
req := p.GetPrepareRequest().(*prepareRequest)
|
req := p.GetPrepareRequest().(*prepareRequest)
|
||||||
if s.stateRootEnabled() {
|
if s.stateRootEnabled() {
|
||||||
r, err := s.Chain.GetStateRoot(s.dbft.BlockIndex - 1)
|
err := s.verifyStateRootSig(int(p.ValidatorIndex()), req.stateRootSig[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't get local state root: %v", err)
|
return err
|
||||||
}
|
|
||||||
if !r.Equals(&req.proposalStateRoot) {
|
|
||||||
return errors.New("state root mismatch")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Save lastProposal for getVerified().
|
// Save lastProposal for getVerified().
|
||||||
|
@ -411,6 +431,14 @@ func (s *service) verifyRequest(p payload.ConsensusPayload) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) verifyResponse(p payload.ConsensusPayload) error {
|
||||||
|
if !s.stateRootEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := p.GetPrepareResponse().(*prepareResponse)
|
||||||
|
return s.verifyStateRootSig(int(p.ValidatorIndex()), resp.stateRootSig[:])
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) processBlock(b block.Block) {
|
func (s *service) processBlock(b block.Block) {
|
||||||
bb := &b.(*neoBlock).Block
|
bb := &b.(*neoBlock).Block
|
||||||
bb.Script = *(s.getBlockWitness(bb))
|
bb.Script = *(s.getBlockWitness(bb))
|
||||||
|
@ -422,36 +450,26 @@ func (s *service) processBlock(b block.Block) {
|
||||||
s.log.Warn("error on add block", zap.Error(err))
|
s.log.Warn("error on add block", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var rb *state.MPTRootBase
|
|
||||||
for _, p := range s.dbft.PreparationPayloads {
|
|
||||||
if p != nil && p.Type() == payload.PrepareRequestType {
|
|
||||||
rb = &p.GetPrepareRequest().(*prepareRequest).proposalStateRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w := s.getWitness(func(p payload.Commit) []byte { return p.(*commit).stateSig[:] })
|
|
||||||
r := &state.MPTRoot{
|
|
||||||
MPTRootBase: *rb,
|
|
||||||
Witness: w,
|
|
||||||
}
|
|
||||||
if err := s.Chain.AddStateRoot(r); err != nil {
|
|
||||||
s.log.Warn("errors while adding state root", zap.Error(err))
|
|
||||||
}
|
|
||||||
s.Broadcast(r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) getBlockWitness(_ *coreb.Block) *transaction.Witness {
|
func (s *service) getBlockWitness(_ *coreb.Block) *transaction.Witness {
|
||||||
return s.getWitness(func(p payload.Commit) []byte { return p.Signature() })
|
return s.getWitness(func(ctx dbft.Context, i int) []byte {
|
||||||
|
if p := ctx.CommitPayloads[i]; p != nil && p.ViewNumber() == ctx.ViewNumber {
|
||||||
|
return p.GetCommit().Signature()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) getWitness(f func(p payload.Commit) []byte) *transaction.Witness {
|
func (s *service) getWitness(f func(dbft.Context, int) []byte) *transaction.Witness {
|
||||||
dctx := s.dbft.Context
|
dctx := s.dbft.Context
|
||||||
pubs := convertKeys(dctx.Validators)
|
pubs := convertKeys(dctx.Validators)
|
||||||
sigs := make(map[*keys.PublicKey][]byte)
|
sigs := make(map[*keys.PublicKey][]byte)
|
||||||
|
|
||||||
for i := range pubs {
|
for i := range pubs {
|
||||||
if p := dctx.CommitPayloads[i]; p != nil && p.ViewNumber() == dctx.ViewNumber {
|
sig := f(dctx, i)
|
||||||
sigs[pubs[i]] = f(p.GetCommit())
|
if sig != nil {
|
||||||
|
sigs[pubs[i]] = sig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,7 +621,29 @@ func convertKeys(validators []crypto.PublicKey) (pubs []*keys.PublicKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBlockFromContext(ctx *dbft.Context) block.Block {
|
func (s *service) newBlockFromContext(ctx *dbft.Context) block.Block {
|
||||||
|
if s.stateRootEnabled() {
|
||||||
|
// This is being called when we're ready to commit, so we can safely
|
||||||
|
// relay stateroot here.
|
||||||
|
stateRoot, err := s.Chain.GetStateRoot(s.dbft.Context.BlockIndex - 1)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("can't get stateroot", zap.Uint32("block", s.dbft.Context.BlockIndex-1))
|
||||||
|
}
|
||||||
|
r := stateRoot.MPTRoot
|
||||||
|
r.Witness = s.getWitness(func(ctx dbft.Context, i int) []byte {
|
||||||
|
if p := ctx.PreparationPayloads[i]; p != nil && p.ViewNumber() == ctx.ViewNumber {
|
||||||
|
if int(ctx.PrimaryIndex) == i {
|
||||||
|
return p.GetPrepareRequest().(*prepareRequest).stateRootSig[:]
|
||||||
|
}
|
||||||
|
return p.GetPrepareResponse().(*prepareResponse).stateRootSig[:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err := s.Chain.AddStateRoot(&r); err != nil {
|
||||||
|
s.log.Warn("errors while adding state root", zap.Error(err))
|
||||||
|
}
|
||||||
|
s.Broadcast(&r)
|
||||||
|
}
|
||||||
block := new(neoBlock)
|
block := new(neoBlock)
|
||||||
if len(ctx.TransactionHashes) == 0 {
|
if len(ctx.TransactionHashes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -49,20 +49,24 @@ func TestService_GetVerified(t *testing.T) {
|
||||||
|
|
||||||
// Everyone sends a message.
|
// Everyone sends a message.
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
p := new(Payload)
|
p := srv.newPayload().(*Payload)
|
||||||
p.message = &message{}
|
p.SetHeight(1)
|
||||||
|
p.SetValidatorIndex(uint16(i))
|
||||||
|
priv, _ := getTestValidator(i)
|
||||||
|
// To properly sign stateroot in prepare request.
|
||||||
|
srv.dbft.Priv = priv
|
||||||
// One PrepareRequest and three ChangeViews.
|
// One PrepareRequest and three ChangeViews.
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
p.SetType(payload.PrepareRequestType)
|
p.SetType(payload.PrepareRequestType)
|
||||||
p.SetPayload(&prepareRequest{transactionHashes: hashes, minerTx: *newMinerTx(999)})
|
preq := srv.newPrepareRequest().(*prepareRequest)
|
||||||
|
preq.transactionHashes = hashes
|
||||||
|
preq.minerTx = *newMinerTx(999)
|
||||||
|
p.SetPayload(preq)
|
||||||
} else {
|
} else {
|
||||||
p.SetType(payload.ChangeViewType)
|
p.SetType(payload.ChangeViewType)
|
||||||
p.SetPayload(&changeView{newViewNumber: 1, timestamp: uint32(time.Now().Unix())})
|
p.SetPayload(&changeView{newViewNumber: 1, timestamp: uint32(time.Now().Unix())})
|
||||||
}
|
}
|
||||||
p.SetHeight(1)
|
|
||||||
p.SetValidatorIndex(uint16(i))
|
|
||||||
|
|
||||||
priv, _ := getTestValidator(i)
|
|
||||||
require.NoError(t, p.Sign(priv))
|
require.NoError(t, p.Sign(priv))
|
||||||
|
|
||||||
// Skip srv.OnPayload, because the service is not really started.
|
// Skip srv.OnPayload, because the service is not really started.
|
||||||
|
|
|
@ -49,6 +49,8 @@ const (
|
||||||
commitType messageType = 0x30
|
commitType messageType = 0x30
|
||||||
recoveryRequestType messageType = 0x40
|
recoveryRequestType messageType = 0x40
|
||||||
recoveryMessageType messageType = 0x41
|
recoveryMessageType messageType = 0x41
|
||||||
|
|
||||||
|
nanoInSec = 1000_000_000
|
||||||
)
|
)
|
||||||
|
|
||||||
// ViewNumber implements payload.ConsensusPayload interface.
|
// ViewNumber implements payload.ConsensusPayload interface.
|
||||||
|
@ -289,11 +291,11 @@ func (m *message) DecodeBinary(r *io.BinReader) {
|
||||||
stateRootEnabled: m.stateRootEnabled,
|
stateRootEnabled: m.stateRootEnabled,
|
||||||
}
|
}
|
||||||
case prepareResponseType:
|
case prepareResponseType:
|
||||||
m.payload = new(prepareResponse)
|
m.payload = &prepareResponse{
|
||||||
case commitType:
|
|
||||||
m.payload = &commit{
|
|
||||||
stateRootEnabled: m.stateRootEnabled,
|
stateRootEnabled: m.stateRootEnabled,
|
||||||
}
|
}
|
||||||
|
case commitType:
|
||||||
|
m.payload = new(commit)
|
||||||
case recoveryRequestType:
|
case recoveryRequestType:
|
||||||
m.payload = new(recoveryRequest)
|
m.payload = new(recoveryRequest)
|
||||||
case recoveryMessageType:
|
case recoveryMessageType:
|
||||||
|
|
|
@ -173,13 +173,13 @@ func testEncodeDecode(srEnabled bool, mt messageType, actual io.Serializable) fu
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommit_Serializable(t *testing.T) {
|
func TestCommit_Serializable(t *testing.T) {
|
||||||
t.Run("WithStateRoot", testEncodeDecode(true, commitType, &commit{stateRootEnabled: true}))
|
testEncodeDecode(false, commitType, &commit{})
|
||||||
t.Run("NoStateRoot", testEncodeDecode(false, commitType, &commit{stateRootEnabled: false}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareResponse_Serializable(t *testing.T) {
|
func TestPrepareResponse_Serializable(t *testing.T) {
|
||||||
resp := randomMessage(t, prepareResponseType)
|
t.Run("WithStateRoot", testEncodeDecode(true, prepareResponseType, &prepareResponse{stateRootEnabled: true}))
|
||||||
testserdes.EncodeDecodeBinary(t, resp, new(prepareResponse))
|
t.Run("NoStateRoot", testEncodeDecode(false, prepareResponseType, &prepareResponse{stateRootEnabled: false}))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareRequest_Serializable(t *testing.T) {
|
func TestPrepareRequest_Serializable(t *testing.T) {
|
||||||
|
@ -231,14 +231,15 @@ func randomMessage(t *testing.T, mt messageType, srEnabled ...bool) io.Serializa
|
||||||
case prepareRequestType:
|
case prepareRequestType:
|
||||||
return randomPrepareRequest(t, srEnabled...)
|
return randomPrepareRequest(t, srEnabled...)
|
||||||
case prepareResponseType:
|
case prepareResponseType:
|
||||||
return &prepareResponse{preparationHash: random.Uint256()}
|
var p = prepareResponse{preparationHash: random.Uint256()}
|
||||||
|
if len(srEnabled) > 0 && srEnabled[0] {
|
||||||
|
p.stateRootEnabled = true
|
||||||
|
random.Fill(p.stateRootSig[:])
|
||||||
|
}
|
||||||
|
return &p
|
||||||
case commitType:
|
case commitType:
|
||||||
var c commit
|
var c commit
|
||||||
random.Fill(c.signature[:])
|
random.Fill(c.signature[:])
|
||||||
if len(srEnabled) > 0 && srEnabled[0] {
|
|
||||||
c.stateRootEnabled = true
|
|
||||||
random.Fill(c.stateSig[:])
|
|
||||||
}
|
|
||||||
return &c
|
return &c
|
||||||
case recoveryRequestType:
|
case recoveryRequestType:
|
||||||
return &recoveryRequest{timestamp: rand.Uint32()}
|
return &recoveryRequest{timestamp: rand.Uint32()}
|
||||||
|
@ -268,9 +269,7 @@ func randomPrepareRequest(t *testing.T, srEnabled ...bool) *prepareRequest {
|
||||||
|
|
||||||
if len(srEnabled) > 0 && srEnabled[0] {
|
if len(srEnabled) > 0 && srEnabled[0] {
|
||||||
req.stateRootEnabled = true
|
req.stateRootEnabled = true
|
||||||
req.proposalStateRoot.Index = rand.Uint32()
|
random.Fill(req.stateRootSig[:])
|
||||||
req.proposalStateRoot.PrevHash = random.Uint256()
|
|
||||||
req.proposalStateRoot.Root = random.Uint256()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return req
|
return req
|
||||||
|
@ -318,9 +317,10 @@ func randomRecoveryMessage(t *testing.T, srEnabled ...bool) *recoveryMessage {
|
||||||
if len(srEnabled) > 0 && srEnabled[0] {
|
if len(srEnabled) > 0 && srEnabled[0] {
|
||||||
rec.stateRootEnabled = true
|
rec.stateRootEnabled = true
|
||||||
rec.prepareRequest.stateRootEnabled = true
|
rec.prepareRequest.stateRootEnabled = true
|
||||||
for _, c := range rec.commitPayloads {
|
random.Fill(prepReq.stateRootSig[:])
|
||||||
c.stateRootEnabled = true
|
for _, p := range rec.preparationPayloads {
|
||||||
random.Fill(c.StateSignature[:])
|
p.stateRootEnabled = true
|
||||||
|
random.Fill(p.StateRootSig[:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rec
|
return rec
|
||||||
|
|
|
@ -2,7 +2,6 @@ package consensus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/dbft/payload"
|
"github.com/nspcc-dev/dbft/payload"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
@ -15,7 +14,7 @@ type prepareRequest struct {
|
||||||
transactionHashes []util.Uint256
|
transactionHashes []util.Uint256
|
||||||
minerTx transaction.Transaction
|
minerTx transaction.Transaction
|
||||||
nextConsensus util.Uint160
|
nextConsensus util.Uint160
|
||||||
proposalStateRoot state.MPTRootBase
|
stateRootSig [signatureSize]byte
|
||||||
|
|
||||||
stateRootEnabled bool
|
stateRootEnabled bool
|
||||||
}
|
}
|
||||||
|
@ -30,7 +29,7 @@ func (p *prepareRequest) EncodeBinary(w *io.BinWriter) {
|
||||||
w.WriteArray(p.transactionHashes)
|
w.WriteArray(p.transactionHashes)
|
||||||
p.minerTx.EncodeBinary(w)
|
p.minerTx.EncodeBinary(w)
|
||||||
if p.stateRootEnabled {
|
if p.stateRootEnabled {
|
||||||
p.proposalStateRoot.EncodeBinary(w)
|
w.WriteBytes(p.stateRootSig[:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ func (p *prepareRequest) DecodeBinary(r *io.BinReader) {
|
||||||
r.ReadArray(&p.transactionHashes)
|
r.ReadArray(&p.transactionHashes)
|
||||||
p.minerTx.DecodeBinary(r)
|
p.minerTx.DecodeBinary(r)
|
||||||
if p.stateRootEnabled {
|
if p.stateRootEnabled {
|
||||||
p.proposalStateRoot.DecodeBinary(r)
|
r.ReadBytes(p.stateRootSig[:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ import (
|
||||||
// prepareResponse represents dBFT PrepareResponse message.
|
// prepareResponse represents dBFT PrepareResponse message.
|
||||||
type prepareResponse struct {
|
type prepareResponse struct {
|
||||||
preparationHash util.Uint256
|
preparationHash util.Uint256
|
||||||
|
stateRootSig [signatureSize]byte
|
||||||
|
|
||||||
|
stateRootEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ payload.PrepareResponse = (*prepareResponse)(nil)
|
var _ payload.PrepareResponse = (*prepareResponse)(nil)
|
||||||
|
@ -16,11 +19,17 @@ var _ payload.PrepareResponse = (*prepareResponse)(nil)
|
||||||
// EncodeBinary implements io.Serializable interface.
|
// EncodeBinary implements io.Serializable interface.
|
||||||
func (p *prepareResponse) EncodeBinary(w *io.BinWriter) {
|
func (p *prepareResponse) EncodeBinary(w *io.BinWriter) {
|
||||||
w.WriteBytes(p.preparationHash[:])
|
w.WriteBytes(p.preparationHash[:])
|
||||||
|
if p.stateRootEnabled {
|
||||||
|
w.WriteBytes(p.stateRootSig[:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeBinary implements io.Serializable interface.
|
// DecodeBinary implements io.Serializable interface.
|
||||||
func (p *prepareResponse) DecodeBinary(r *io.BinReader) {
|
func (p *prepareResponse) DecodeBinary(r *io.BinReader) {
|
||||||
r.ReadBytes(p.preparationHash[:])
|
r.ReadBytes(p.preparationHash[:])
|
||||||
|
if p.stateRootEnabled {
|
||||||
|
r.ReadBytes(p.stateRootSig[:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreparationHash implements payload.PrepareResponse interface.
|
// PreparationHash implements payload.PrepareResponse interface.
|
||||||
|
|
|
@ -32,15 +32,15 @@ type (
|
||||||
ViewNumber byte
|
ViewNumber byte
|
||||||
ValidatorIndex uint16
|
ValidatorIndex uint16
|
||||||
Signature [signatureSize]byte
|
Signature [signatureSize]byte
|
||||||
StateSignature [signatureSize]byte
|
|
||||||
InvocationScript []byte
|
InvocationScript []byte
|
||||||
|
|
||||||
stateRootEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preparationCompact struct {
|
preparationCompact struct {
|
||||||
ValidatorIndex uint16
|
ValidatorIndex uint16
|
||||||
InvocationScript []byte
|
InvocationScript []byte
|
||||||
|
StateRootSig [signatureSize]byte
|
||||||
|
|
||||||
|
stateRootEnabled bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,17 +72,17 @@ func (m *recoveryMessage) DecodeBinary(r *io.BinReader) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ReadArray(&m.preparationPayloads)
|
|
||||||
lu := r.ReadVarUint()
|
lu := r.ReadVarUint()
|
||||||
if lu > state.MaxValidatorsVoted {
|
if lu > state.MaxValidatorsVoted {
|
||||||
r.Err = errors.New("too many preparation payloads")
|
r.Err = errors.New("too many preparation payloads")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.commitPayloads = make([]*commitCompact, lu)
|
m.preparationPayloads = make([]*preparationCompact, lu)
|
||||||
for i := uint64(0); i < lu; i++ {
|
for i := uint64(0); i < lu; i++ {
|
||||||
m.commitPayloads[i] = &commitCompact{stateRootEnabled: m.stateRootEnabled}
|
m.preparationPayloads[i] = &preparationCompact{stateRootEnabled: m.stateRootEnabled}
|
||||||
m.commitPayloads[i].DecodeBinary(r)
|
m.preparationPayloads[i].DecodeBinary(r)
|
||||||
}
|
}
|
||||||
|
r.ReadArray(&m.commitPayloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeBinary implements io.Serializable interface.
|
// EncodeBinary implements io.Serializable interface.
|
||||||
|
@ -127,9 +127,6 @@ func (p *commitCompact) DecodeBinary(r *io.BinReader) {
|
||||||
p.ViewNumber = r.ReadB()
|
p.ViewNumber = r.ReadB()
|
||||||
p.ValidatorIndex = r.ReadU16LE()
|
p.ValidatorIndex = r.ReadU16LE()
|
||||||
r.ReadBytes(p.Signature[:])
|
r.ReadBytes(p.Signature[:])
|
||||||
if p.stateRootEnabled {
|
|
||||||
r.ReadBytes(p.StateSignature[:])
|
|
||||||
}
|
|
||||||
p.InvocationScript = r.ReadVarBytes(1024)
|
p.InvocationScript = r.ReadVarBytes(1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,9 +135,6 @@ func (p *commitCompact) EncodeBinary(w *io.BinWriter) {
|
||||||
w.WriteB(p.ViewNumber)
|
w.WriteB(p.ViewNumber)
|
||||||
w.WriteU16LE(p.ValidatorIndex)
|
w.WriteU16LE(p.ValidatorIndex)
|
||||||
w.WriteBytes(p.Signature[:])
|
w.WriteBytes(p.Signature[:])
|
||||||
if p.stateRootEnabled {
|
|
||||||
w.WriteBytes(p.StateSignature[:])
|
|
||||||
}
|
|
||||||
w.WriteVarBytes(p.InvocationScript)
|
w.WriteVarBytes(p.InvocationScript)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,12 +142,18 @@ func (p *commitCompact) EncodeBinary(w *io.BinWriter) {
|
||||||
func (p *preparationCompact) DecodeBinary(r *io.BinReader) {
|
func (p *preparationCompact) DecodeBinary(r *io.BinReader) {
|
||||||
p.ValidatorIndex = r.ReadU16LE()
|
p.ValidatorIndex = r.ReadU16LE()
|
||||||
p.InvocationScript = r.ReadVarBytes(1024)
|
p.InvocationScript = r.ReadVarBytes(1024)
|
||||||
|
if p.stateRootEnabled {
|
||||||
|
r.ReadBytes(p.StateRootSig[:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeBinary implements io.Serializable interface.
|
// EncodeBinary implements io.Serializable interface.
|
||||||
func (p *preparationCompact) EncodeBinary(w *io.BinWriter) {
|
func (p *preparationCompact) EncodeBinary(w *io.BinWriter) {
|
||||||
w.WriteU16LE(p.ValidatorIndex)
|
w.WriteU16LE(p.ValidatorIndex)
|
||||||
w.WriteVarBytes(p.InvocationScript)
|
w.WriteVarBytes(p.InvocationScript)
|
||||||
|
if p.stateRootEnabled {
|
||||||
|
w.WriteBytes(p.StateRootSig[:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddPayload implements payload.RecoveryMessage interface.
|
// AddPayload implements payload.RecoveryMessage interface.
|
||||||
|
@ -170,13 +170,17 @@ func (m *recoveryMessage) AddPayload(p payload.ConsensusPayload) {
|
||||||
h := p.Hash()
|
h := p.Hash()
|
||||||
m.preparationHash = &h
|
m.preparationHash = &h
|
||||||
m.preparationPayloads = append(m.preparationPayloads, &preparationCompact{
|
m.preparationPayloads = append(m.preparationPayloads, &preparationCompact{
|
||||||
|
stateRootEnabled: m.stateRootEnabled,
|
||||||
ValidatorIndex: p.ValidatorIndex(),
|
ValidatorIndex: p.ValidatorIndex(),
|
||||||
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
||||||
|
StateRootSig: p.GetPrepareRequest().(*prepareRequest).stateRootSig,
|
||||||
})
|
})
|
||||||
case payload.PrepareResponseType:
|
case payload.PrepareResponseType:
|
||||||
m.preparationPayloads = append(m.preparationPayloads, &preparationCompact{
|
m.preparationPayloads = append(m.preparationPayloads, &preparationCompact{
|
||||||
|
stateRootEnabled: m.stateRootEnabled,
|
||||||
ValidatorIndex: p.ValidatorIndex(),
|
ValidatorIndex: p.ValidatorIndex(),
|
||||||
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
||||||
|
StateRootSig: p.GetPrepareResponse().(*prepareResponse).stateRootSig,
|
||||||
})
|
})
|
||||||
|
|
||||||
if m.preparationHash == nil {
|
if m.preparationHash == nil {
|
||||||
|
@ -187,16 +191,14 @@ func (m *recoveryMessage) AddPayload(p payload.ConsensusPayload) {
|
||||||
m.changeViewPayloads = append(m.changeViewPayloads, &changeViewCompact{
|
m.changeViewPayloads = append(m.changeViewPayloads, &changeViewCompact{
|
||||||
ValidatorIndex: p.ValidatorIndex(),
|
ValidatorIndex: p.ValidatorIndex(),
|
||||||
OriginalViewNumber: p.ViewNumber(),
|
OriginalViewNumber: p.ViewNumber(),
|
||||||
Timestamp: p.GetChangeView().Timestamp(),
|
Timestamp: p.GetChangeView().(*changeView).timestamp,
|
||||||
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
||||||
})
|
})
|
||||||
case payload.CommitType:
|
case payload.CommitType:
|
||||||
m.commitPayloads = append(m.commitPayloads, &commitCompact{
|
m.commitPayloads = append(m.commitPayloads, &commitCompact{
|
||||||
stateRootEnabled: m.stateRootEnabled,
|
|
||||||
ValidatorIndex: p.ValidatorIndex(),
|
ValidatorIndex: p.ValidatorIndex(),
|
||||||
ViewNumber: p.ViewNumber(),
|
ViewNumber: p.ViewNumber(),
|
||||||
Signature: p.GetCommit().(*commit).signature,
|
Signature: p.GetCommit().(*commit).signature,
|
||||||
StateSignature: p.GetCommit().(*commit).stateSig,
|
|
||||||
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
InvocationScript: p.(*Payload).Witness.InvocationScript,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -239,6 +241,9 @@ func (m *recoveryMessage) GetPrepareResponses(p payload.ConsensusPayload, valida
|
||||||
for i, resp := range m.preparationPayloads {
|
for i, resp := range m.preparationPayloads {
|
||||||
r := fromPayload(prepareResponseType, p.(*Payload), &prepareResponse{
|
r := fromPayload(prepareResponseType, p.(*Payload), &prepareResponse{
|
||||||
preparationHash: *m.preparationHash,
|
preparationHash: *m.preparationHash,
|
||||||
|
stateRootSig: resp.StateRootSig,
|
||||||
|
|
||||||
|
stateRootEnabled: m.stateRootEnabled,
|
||||||
})
|
})
|
||||||
r.SetValidatorIndex(resp.ValidatorIndex)
|
r.SetValidatorIndex(resp.ValidatorIndex)
|
||||||
r.Witness.InvocationScript = resp.InvocationScript
|
r.Witness.InvocationScript = resp.InvocationScript
|
||||||
|
@ -277,9 +282,6 @@ func (m *recoveryMessage) GetCommits(p payload.ConsensusPayload, validators []cr
|
||||||
for i, c := range m.commitPayloads {
|
for i, c := range m.commitPayloads {
|
||||||
cc := fromPayload(commitType, p.(*Payload), &commit{
|
cc := fromPayload(commitType, p.(*Payload), &commit{
|
||||||
signature: c.Signature,
|
signature: c.Signature,
|
||||||
stateSig: c.StateSignature,
|
|
||||||
|
|
||||||
stateRootEnabled: m.stateRootEnabled,
|
|
||||||
})
|
})
|
||||||
cc.SetValidatorIndex(c.ValidatorIndex)
|
cc.SetValidatorIndex(c.ValidatorIndex)
|
||||||
cc.Witness.InvocationScript = c.InvocationScript
|
cc.Witness.InvocationScript = c.InvocationScript
|
||||||
|
|
|
@ -23,7 +23,7 @@ func (m *recoveryRequest) EncodeBinary(w *io.BinWriter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp implements payload.RecoveryRequest interface.
|
// Timestamp implements payload.RecoveryRequest interface.
|
||||||
func (m *recoveryRequest) Timestamp() uint32 { return m.timestamp }
|
func (m *recoveryRequest) Timestamp() uint64 { return uint64(m.timestamp) * nanoInSec }
|
||||||
|
|
||||||
// SetTimestamp implements payload.RecoveryRequest interface.
|
// SetTimestamp implements payload.RecoveryRequest interface.
|
||||||
func (m *recoveryRequest) SetTimestamp(ts uint32) { m.timestamp = ts }
|
func (m *recoveryRequest) SetTimestamp(ts uint64) { m.timestamp = uint32(ts / nanoInSec) }
|
||||||
|
|
|
@ -9,6 +9,6 @@ import (
|
||||||
func TestRecoveryRequest_Setters(t *testing.T) {
|
func TestRecoveryRequest_Setters(t *testing.T) {
|
||||||
var r recoveryRequest
|
var r recoveryRequest
|
||||||
|
|
||||||
r.SetTimestamp(123)
|
r.SetTimestamp(123 * nanoInSec)
|
||||||
require.EqualValues(t, 123, r.Timestamp())
|
require.EqualValues(t, 123*nanoInSec, r.Timestamp())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue