mirror of https://github.com/poanetwork/gecko.git
Merge branch 'master' into ansible-service
This commit is contained in:
commit
f737368d65
|
@ -0,0 +1,6 @@
|
||||||
|
# https://editorconfig.org/
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_newspace = true
|
|
@ -234,7 +234,7 @@ func (s *Voting) Get(validatorID ids.ShortID, chainID ids.ID, requestID uint32,
|
||||||
peer, exists := s.conns.GetPeerID(validatorID)
|
peer, exists := s.conns.GetPeerID(validatorID)
|
||||||
if !exists {
|
if !exists {
|
||||||
s.log.Debug("attempted to send a Get message to a disconnected validator: %s", validatorID)
|
s.log.Debug("attempted to send a Get message to a disconnected validator: %s", validatorID)
|
||||||
s.executor.Add(func() { s.router.GetFailed(validatorID, chainID, requestID, containerID) })
|
s.executor.Add(func() { s.router.GetFailed(validatorID, chainID, requestID) })
|
||||||
return // Validator is not connected
|
return // Validator is not connected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
#!/bin/bash -e
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
# Ted: contact me when you make any changes
|
# Ted: contact me when you make any changes
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
#!/bin/bash -e
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
SRC_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
SRC_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||||
export GOPATH="$SRC_DIR/.build_image_gopath"
|
export GOPATH="$SRC_DIR/.build_image_gopath"
|
||||||
WORKPREFIX="$GOPATH/src/github.com/ava-labs/"
|
WORKPREFIX="$GOPATH/src/github.com/ava-labs/"
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
#!/bin/bash -e
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
# Ted: contact me when you make any changes
|
# Ted: contact me when you make any changes
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ type snowmanBlock struct {
|
||||||
// block that this node contains. For the genesis, this value will be nil
|
// block that this node contains. For the genesis, this value will be nil
|
||||||
blk Block
|
blk Block
|
||||||
|
|
||||||
// shouldFalter is set to true if this node, and all its decendants received
|
// shouldFalter is set to true if this node, and all its descendants received
|
||||||
// less than Alpha votes
|
// less than Alpha votes
|
||||||
shouldFalter bool
|
shouldFalter bool
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,7 @@ func (ts *Topological) Preference() ids.ID { return ts.tail }
|
||||||
// During the sort, votes are pushed towards the genesis. To prevent interating
|
// During the sort, votes are pushed towards the genesis. To prevent interating
|
||||||
// over all blocks that had unsuccessful polls, we set a flag on the block to
|
// over all blocks that had unsuccessful polls, we set a flag on the block to
|
||||||
// know that any future traversal through that block should register an
|
// know that any future traversal through that block should register an
|
||||||
// unsuccessful poll on that block and every decendant block.
|
// unsuccessful poll on that block and every descendant block.
|
||||||
//
|
//
|
||||||
// The complexity of this function is:
|
// The complexity of this function is:
|
||||||
// - Runtime = 3 * |live set| + |votes|
|
// - Runtime = 3 * |live set| + |votes|
|
||||||
|
@ -408,7 +408,7 @@ func (ts *Topological) getPreferredDecendent(blkID ids.ID) ids.ID {
|
||||||
|
|
||||||
// accept the preferred child of the provided snowman block. By accepting the
|
// accept the preferred child of the provided snowman block. By accepting the
|
||||||
// preferred child, all other children will be rejected. When these children are
|
// preferred child, all other children will be rejected. When these children are
|
||||||
// rejected, all their decendants will be rejected.
|
// rejected, all their descendants will be rejected.
|
||||||
func (ts *Topological) accept(n *snowmanBlock) {
|
func (ts *Topological) accept(n *snowmanBlock) {
|
||||||
// We are finalizing the block's child, so we need to get the preference
|
// We are finalizing the block's child, so we need to get the preference
|
||||||
pref := n.sb.Preference()
|
pref := n.sb.Preference()
|
||||||
|
@ -451,11 +451,11 @@ func (ts *Topological) accept(n *snowmanBlock) {
|
||||||
rejects = append(rejects, childID)
|
rejects = append(rejects, childID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reject all the decendants of the blocks we just rejected
|
// reject all the descendants of the blocks we just rejected
|
||||||
ts.rejectTransitively(rejects)
|
ts.rejectTransitively(rejects)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Takes in a list of rejected ids and rejects all decendants of these IDs
|
// Takes in a list of rejected ids and rejects all descendants of these IDs
|
||||||
func (ts *Topological) rejectTransitively(rejected []ids.ID) {
|
func (ts *Topological) rejectTransitively(rejected []ids.ID) {
|
||||||
// the rejected array is treated as a queue, with the next element at index
|
// the rejected array is treated as a queue, with the next element at index
|
||||||
// 0 and the last element at the end of the slice.
|
// 0 and the last element at the end of the slice.
|
||||||
|
|
|
@ -30,6 +30,9 @@ type bootstrapper struct {
|
||||||
metrics
|
metrics
|
||||||
common.Bootstrapper
|
common.Bootstrapper
|
||||||
|
|
||||||
|
// vtxReqs prevents asking validators for the same vertex
|
||||||
|
vtxReqs common.Requests
|
||||||
|
|
||||||
pending ids.Set
|
pending ids.Set
|
||||||
finished bool
|
finished bool
|
||||||
onFinished func()
|
onFinished func()
|
||||||
|
@ -90,16 +93,22 @@ func (b *bootstrapper) ForceAccepted(acceptedContainerIDs ids.Set) {
|
||||||
func (b *bootstrapper) Put(vdr ids.ShortID, requestID uint32, vtxID ids.ID, vtxBytes []byte) {
|
func (b *bootstrapper) Put(vdr ids.ShortID, requestID uint32, vtxID ids.ID, vtxBytes []byte) {
|
||||||
b.BootstrapConfig.Context.Log.Verbo("Put called for vertexID %s", vtxID)
|
b.BootstrapConfig.Context.Log.Verbo("Put called for vertexID %s", vtxID)
|
||||||
|
|
||||||
if !b.pending.Contains(vtxID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vtx, err := b.State.ParseVertex(vtxBytes)
|
vtx, err := b.State.ParseVertex(vtxBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.BootstrapConfig.Context.Log.Debug("ParseVertex failed due to %s for block:\n%s",
|
b.BootstrapConfig.Context.Log.Debug("ParseVertex failed due to %s for block:\n%s",
|
||||||
err,
|
err,
|
||||||
formatting.DumpBytes{Bytes: vtxBytes})
|
formatting.DumpBytes{Bytes: vtxBytes})
|
||||||
b.GetFailed(vdr, requestID, vtxID)
|
|
||||||
|
b.GetFailed(vdr, requestID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.pending.Contains(vtx.ID()) {
|
||||||
|
b.BootstrapConfig.Context.Log.Debug("Validator %s sent an unrequested vertex:\n%s",
|
||||||
|
vdr,
|
||||||
|
formatting.DumpBytes{Bytes: vtxBytes})
|
||||||
|
|
||||||
|
b.GetFailed(vdr, requestID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +116,16 @@ func (b *bootstrapper) Put(vdr ids.ShortID, requestID uint32, vtxID ids.ID, vtxB
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFailed ...
|
// GetFailed ...
|
||||||
func (b *bootstrapper) GetFailed(_ ids.ShortID, _ uint32, vtxID ids.ID) { b.sendRequest(vtxID) }
|
func (b *bootstrapper) GetFailed(vdr ids.ShortID, requestID uint32) {
|
||||||
|
vtxID, ok := b.vtxReqs.Remove(vdr, requestID)
|
||||||
|
if !ok {
|
||||||
|
b.BootstrapConfig.Context.Log.Debug("GetFailed called without sending the corresponding Get message from %s",
|
||||||
|
vdr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.sendRequest(vtxID)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *bootstrapper) fetch(vtxID ids.ID) {
|
func (b *bootstrapper) fetch(vtxID ids.ID) {
|
||||||
if b.pending.Contains(vtxID) {
|
if b.pending.Contains(vtxID) {
|
||||||
|
@ -131,6 +149,9 @@ func (b *bootstrapper) sendRequest(vtxID ids.ID) {
|
||||||
validatorID := validators[0].ID()
|
validatorID := validators[0].ID()
|
||||||
b.RequestID++
|
b.RequestID++
|
||||||
|
|
||||||
|
b.vtxReqs.RemoveAny(vtxID)
|
||||||
|
b.vtxReqs.Add(validatorID, b.RequestID, vtxID)
|
||||||
|
|
||||||
b.pending.Add(vtxID)
|
b.pending.Add(vtxID)
|
||||||
b.BootstrapConfig.Sender.Get(validatorID, b.RequestID, vtxID)
|
b.BootstrapConfig.Sender.Get(validatorID, b.RequestID, vtxID)
|
||||||
|
|
||||||
|
|
|
@ -289,7 +289,6 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) {
|
||||||
bs.ForceAccepted(acceptedIDs)
|
bs.ForceAccepted(acceptedIDs)
|
||||||
|
|
||||||
state.getVertex = nil
|
state.getVertex = nil
|
||||||
sender.GetF = nil
|
|
||||||
|
|
||||||
state.parseVertex = func(vtxBytes []byte) (avalanche.Vertex, error) {
|
state.parseVertex = func(vtxBytes []byte) (avalanche.Vertex, error) {
|
||||||
switch {
|
switch {
|
||||||
|
@ -1008,3 +1007,111 @@ func TestBootstrapperPartialFetch(t *testing.T) {
|
||||||
t.Fatalf("wrong number pending")
|
t.Fatalf("wrong number pending")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBootstrapperWrongIDByzantineResponse(t *testing.T) {
|
||||||
|
config, peerID, sender, state, _ := newConfig(t)
|
||||||
|
|
||||||
|
vtxID0 := ids.Empty.Prefix(0)
|
||||||
|
vtxID1 := ids.Empty.Prefix(1)
|
||||||
|
|
||||||
|
vtxBytes0 := []byte{0}
|
||||||
|
vtxBytes1 := []byte{1}
|
||||||
|
|
||||||
|
vtx0 := &Vtx{
|
||||||
|
id: vtxID0,
|
||||||
|
height: 0,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: vtxBytes0,
|
||||||
|
}
|
||||||
|
vtx1 := &Vtx{
|
||||||
|
id: vtxID1,
|
||||||
|
height: 0,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: vtxBytes1,
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := bootstrapper{}
|
||||||
|
bs.metrics.Initialize(config.Context.Log, fmt.Sprintf("gecko_%s", config.Context.ChainID), prometheus.NewRegistry())
|
||||||
|
bs.Initialize(config)
|
||||||
|
|
||||||
|
acceptedIDs := ids.Set{}
|
||||||
|
acceptedIDs.Add(
|
||||||
|
vtxID0,
|
||||||
|
)
|
||||||
|
|
||||||
|
state.getVertex = func(vtxID ids.ID) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtxID0):
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
default:
|
||||||
|
t.Fatal(errUnknownVertex)
|
||||||
|
panic(errUnknownVertex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestID := new(uint32)
|
||||||
|
sender.GetF = func(vdr ids.ShortID, reqID uint32, vtxID ids.ID) {
|
||||||
|
if !vdr.Equals(peerID) {
|
||||||
|
t.Fatalf("Should have requested vertex from %s, requested from %s", peerID, vdr)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtxID0):
|
||||||
|
default:
|
||||||
|
t.Fatalf("Requested unknown vertex")
|
||||||
|
}
|
||||||
|
|
||||||
|
*requestID = reqID
|
||||||
|
}
|
||||||
|
|
||||||
|
bs.ForceAccepted(acceptedIDs)
|
||||||
|
|
||||||
|
state.getVertex = nil
|
||||||
|
sender.GetF = nil
|
||||||
|
|
||||||
|
state.parseVertex = func(vtxBytes []byte) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(vtxBytes, vtxBytes0):
|
||||||
|
return vtx0, nil
|
||||||
|
case bytes.Equal(vtxBytes, vtxBytes1):
|
||||||
|
return vtx1, nil
|
||||||
|
}
|
||||||
|
t.Fatal(errParsedUnknownVertex)
|
||||||
|
return nil, errParsedUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
state.getVertex = func(vtxID ids.ID) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtxID0):
|
||||||
|
return vtx0, nil
|
||||||
|
case vtxID.Equals(vtxID1):
|
||||||
|
return vtx1, nil
|
||||||
|
default:
|
||||||
|
t.Fatal(errUnknownVertex)
|
||||||
|
panic(errUnknownVertex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finished := new(bool)
|
||||||
|
bs.onFinished = func() { *finished = true }
|
||||||
|
sender.CantGet = false
|
||||||
|
|
||||||
|
bs.Put(peerID, *requestID, vtxID0, vtxBytes1)
|
||||||
|
|
||||||
|
sender.CantGet = true
|
||||||
|
|
||||||
|
bs.Put(peerID, *requestID, vtxID0, vtxBytes0)
|
||||||
|
|
||||||
|
state.parseVertex = nil
|
||||||
|
state.edge = nil
|
||||||
|
bs.onFinished = nil
|
||||||
|
|
||||||
|
if !*finished {
|
||||||
|
t.Fatalf("Bootstrapping should have finished")
|
||||||
|
}
|
||||||
|
if vtx0.Status() != choices.Accepted {
|
||||||
|
t.Fatalf("Vertex should be accepted")
|
||||||
|
}
|
||||||
|
if vtx1.Status() != choices.Processing {
|
||||||
|
t.Fatalf("Vertex should be processing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -76,10 +76,8 @@ func (i *issuer) Update() {
|
||||||
}
|
}
|
||||||
|
|
||||||
i.t.RequestID++
|
i.t.RequestID++
|
||||||
polled := false
|
|
||||||
if numVdrs := len(vdrs); numVdrs == p.K && i.t.polls.Add(i.t.RequestID, vdrSet.Len()) {
|
if numVdrs := len(vdrs); numVdrs == p.K && i.t.polls.Add(i.t.RequestID, vdrSet.Len()) {
|
||||||
i.t.Config.Sender.PushQuery(vdrSet, i.t.RequestID, vtxID, i.vtx.Bytes())
|
i.t.Config.Sender.PushQuery(vdrSet, i.t.RequestID, vtxID, i.vtx.Bytes())
|
||||||
polled = true
|
|
||||||
} else if numVdrs < p.K {
|
} else if numVdrs < p.K {
|
||||||
i.t.Config.Context.Log.Error("Query for %s was dropped due to an insufficient number of validators", vtxID)
|
i.t.Config.Context.Log.Error("Query for %s was dropped due to an insufficient number of validators", vtxID)
|
||||||
}
|
}
|
||||||
|
@ -89,10 +87,8 @@ func (i *issuer) Update() {
|
||||||
i.t.txBlocked.Fulfill(tx.ID())
|
i.t.txBlocked.Fulfill(tx.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
if polled && len(i.t.polls.m) < i.t.Params.ConcurrentRepolls {
|
|
||||||
i.t.repoll()
|
i.t.repoll()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
type vtxIssuer struct{ i *issuer }
|
type vtxIssuer struct{ i *issuer }
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,10 @@ type Transitive struct {
|
||||||
polls polls // track people I have asked for their preference
|
polls polls // track people I have asked for their preference
|
||||||
|
|
||||||
// vtxReqs prevents asking validators for the same vertex
|
// vtxReqs prevents asking validators for the same vertex
|
||||||
|
vtxReqs common.Requests
|
||||||
|
|
||||||
// missingTxs tracks transaction that are missing
|
// missingTxs tracks transaction that are missing
|
||||||
vtxReqs, missingTxs, pending ids.Set
|
missingTxs, pending ids.Set
|
||||||
|
|
||||||
// vtxBlocked tracks operations that are blocked on vertices
|
// vtxBlocked tracks operations that are blocked on vertices
|
||||||
// txBlocked tracks operations that are blocked on transactions
|
// txBlocked tracks operations that are blocked on transactions
|
||||||
|
@ -115,22 +117,27 @@ func (t *Transitive) Put(vdr ids.ShortID, requestID uint32, vtxID ids.ID, vtxByt
|
||||||
t.Config.Context.Log.Debug("ParseVertex failed due to %s for block:\n%s",
|
t.Config.Context.Log.Debug("ParseVertex failed due to %s for block:\n%s",
|
||||||
err,
|
err,
|
||||||
formatting.DumpBytes{Bytes: vtxBytes})
|
formatting.DumpBytes{Bytes: vtxBytes})
|
||||||
t.GetFailed(vdr, requestID, vtxID)
|
t.GetFailed(vdr, requestID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.insertFrom(vdr, vtx)
|
t.insertFrom(vdr, vtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFailed implements the Engine interface
|
// GetFailed implements the Engine interface
|
||||||
func (t *Transitive) GetFailed(vdr ids.ShortID, requestID uint32, vtxID ids.ID) {
|
func (t *Transitive) GetFailed(vdr ids.ShortID, requestID uint32) {
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.bootstrapper.GetFailed(vdr, requestID, vtxID)
|
t.bootstrapper.GetFailed(vdr, requestID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vtxID, ok := t.vtxReqs.Remove(vdr, requestID)
|
||||||
|
if !ok {
|
||||||
|
t.Config.Context.Log.Warn("GetFailed called without sending the corresponding Get message from %s",
|
||||||
|
vdr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.pending.Remove(vtxID)
|
|
||||||
t.vtxBlocked.Abandon(vtxID)
|
t.vtxBlocked.Abandon(vtxID)
|
||||||
t.vtxReqs.Remove(vtxID)
|
|
||||||
|
|
||||||
if t.vtxReqs.Len() == 0 {
|
if t.vtxReqs.Len() == 0 {
|
||||||
for _, txID := range t.missingTxs.List() {
|
for _, txID := range t.missingTxs.List() {
|
||||||
|
@ -142,7 +149,6 @@ func (t *Transitive) GetFailed(vdr ids.ShortID, requestID uint32, vtxID ids.ID)
|
||||||
// Track performance statistics
|
// Track performance statistics
|
||||||
t.numVtxRequests.Set(float64(t.vtxReqs.Len()))
|
t.numVtxRequests.Set(float64(t.vtxReqs.Len()))
|
||||||
t.numTxRequests.Set(float64(t.missingTxs.Len()))
|
t.numTxRequests.Set(float64(t.missingTxs.Len()))
|
||||||
t.numBlockedVtx.Set(float64(t.pending.Len()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PullQuery implements the Engine interface
|
// PullQuery implements the Engine interface
|
||||||
|
@ -167,14 +173,22 @@ func (t *Transitive) PullQuery(vdr ids.ShortID, requestID uint32, vtxID ids.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushQuery implements the Engine interface
|
// PushQuery implements the Engine interface
|
||||||
func (t *Transitive) PushQuery(vdr ids.ShortID, requestID uint32, vtxID ids.ID, vtx []byte) {
|
func (t *Transitive) PushQuery(vdr ids.ShortID, requestID uint32, vtxID ids.ID, vtxBytes []byte) {
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.Config.Context.Log.Debug("Dropping PushQuery for %s due to bootstrapping", vtxID)
|
t.Config.Context.Log.Debug("Dropping PushQuery for %s due to bootstrapping", vtxID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Put(vdr, requestID, vtxID, vtx)
|
vtx, err := t.Config.State.ParseVertex(vtxBytes)
|
||||||
t.PullQuery(vdr, requestID, vtxID)
|
if err != nil {
|
||||||
|
t.Config.Context.Log.Warn("ParseVertex failed due to %s for block:\n%s",
|
||||||
|
err,
|
||||||
|
formatting.DumpBytes{Bytes: vtxBytes})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.insertFrom(vdr, vtx)
|
||||||
|
|
||||||
|
t.PullQuery(vdr, requestID, vtx.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chits implements the Engine interface
|
// Chits implements the Engine interface
|
||||||
|
@ -220,8 +234,16 @@ func (t *Transitive) Notify(msg common.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transitive) repoll() {
|
func (t *Transitive) repoll() {
|
||||||
|
if len(t.polls.m) >= t.Params.ConcurrentRepolls {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
txs := t.Config.VM.PendingTxs()
|
txs := t.Config.VM.PendingTxs()
|
||||||
t.batch(txs, false /*=force*/, true /*=empty*/)
|
t.batch(txs, false /*=force*/, true /*=empty*/)
|
||||||
|
|
||||||
|
for i := len(t.polls.m); i < t.Params.ConcurrentRepolls; i++ {
|
||||||
|
t.batch(nil, false /*=force*/, true /*=empty*/)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transitive) reinsertFrom(vdr ids.ShortID, vtxID ids.ID) bool {
|
func (t *Transitive) reinsertFrom(vdr ids.ShortID, vtxID ids.ID) bool {
|
||||||
|
@ -266,7 +288,7 @@ func (t *Transitive) insert(vtx avalanche.Vertex) {
|
||||||
vtxID := vtx.ID()
|
vtxID := vtx.ID()
|
||||||
|
|
||||||
t.pending.Add(vtxID)
|
t.pending.Add(vtxID)
|
||||||
t.vtxReqs.Remove(vtxID)
|
t.vtxReqs.RemoveAny(vtxID)
|
||||||
|
|
||||||
i := &issuer{
|
i := &issuer{
|
||||||
t: t,
|
t: t,
|
||||||
|
@ -395,10 +417,10 @@ func (t *Transitive) sendRequest(vdr ids.ShortID, vtxID ids.ID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.vtxReqs.Add(vtxID)
|
t.RequestID++
|
||||||
|
|
||||||
|
t.vtxReqs.Add(vdr, t.RequestID, vtxID)
|
||||||
|
t.Config.Sender.Get(vdr, t.RequestID, vtxID)
|
||||||
|
|
||||||
t.numVtxRequests.Set(float64(t.vtxReqs.Len())) // Tracks performance statistics
|
t.numVtxRequests.Set(float64(t.vtxReqs.Len())) // Tracks performance statistics
|
||||||
|
|
||||||
t.RequestID++
|
|
||||||
t.Config.Sender.Get(vdr, t.RequestID, vtxID)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ func TestEngineShutdown(t *testing.T) {
|
||||||
t.Fatal("Shutting down the Transitive did not shutdown the VM")
|
t.Fatal("Shutting down the Transitive did not shutdown the VM")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEngineAdd(t *testing.T) {
|
func TestEngineAdd(t *testing.T) {
|
||||||
config := DefaultConfig()
|
config := DefaultConfig()
|
||||||
|
|
||||||
|
@ -85,7 +86,9 @@ func TestEngineAdd(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
asked := new(bool)
|
asked := new(bool)
|
||||||
sender.GetF = func(inVdr ids.ShortID, _ uint32, vtxID ids.ID) {
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(inVdr ids.ShortID, requestID uint32, vtxID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
if *asked {
|
if *asked {
|
||||||
t.Fatalf("Asked multiple times")
|
t.Fatalf("Asked multiple times")
|
||||||
}
|
}
|
||||||
|
@ -119,7 +122,7 @@ func TestEngineAdd(t *testing.T) {
|
||||||
|
|
||||||
st.parseVertex = func(b []byte) (avalanche.Vertex, error) { return nil, errFailedParsing }
|
st.parseVertex = func(b []byte) (avalanche.Vertex, error) { return nil, errFailedParsing }
|
||||||
|
|
||||||
te.Put(vdr.ID(), 0, vtx.parents[0].ID(), nil)
|
te.Put(vdr.ID(), *reqID, vtx.parents[0].ID(), nil)
|
||||||
|
|
||||||
st.parseVertex = nil
|
st.parseVertex = nil
|
||||||
|
|
||||||
|
@ -485,7 +488,9 @@ func TestEngineMultipleQuery(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
asked := new(bool)
|
asked := new(bool)
|
||||||
sender.GetF = func(inVdr ids.ShortID, _ uint32, vtxID ids.ID) {
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(inVdr ids.ShortID, requestID uint32, vtxID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
if *asked {
|
if *asked {
|
||||||
t.Fatalf("Asked multiple times")
|
t.Fatalf("Asked multiple times")
|
||||||
}
|
}
|
||||||
|
@ -512,7 +517,7 @@ func TestEngineMultipleQuery(t *testing.T) {
|
||||||
// Should be dropped because the query was marked as failed
|
// Should be dropped because the query was marked as failed
|
||||||
te.Chits(vdr1.ID(), *queryRequestID, s0)
|
te.Chits(vdr1.ID(), *queryRequestID, s0)
|
||||||
|
|
||||||
te.GetFailed(vdr0.ID(), 0, vtx1.ID())
|
te.GetFailed(vdr0.ID(), *reqID)
|
||||||
|
|
||||||
if vtx0.Status() != choices.Accepted {
|
if vtx0.Status() != choices.Accepted {
|
||||||
t.Fatalf("Should have executed vertex")
|
t.Fatalf("Should have executed vertex")
|
||||||
|
@ -598,6 +603,12 @@ func TestEngineAbandonResponse(t *testing.T) {
|
||||||
st := &stateTest{t: t}
|
st := &stateTest{t: t}
|
||||||
config.State = st
|
config.State = st
|
||||||
|
|
||||||
|
sender := &common.SenderTest{}
|
||||||
|
sender.T = t
|
||||||
|
config.Sender = sender
|
||||||
|
|
||||||
|
sender.Default(true)
|
||||||
|
|
||||||
gVtx := &Vtx{
|
gVtx := &Vtx{
|
||||||
id: GenerateID(),
|
id: GenerateID(),
|
||||||
status: choices.Accepted,
|
status: choices.Accepted,
|
||||||
|
@ -629,8 +640,13 @@ func TestEngineAbandonResponse(t *testing.T) {
|
||||||
te.Initialize(config)
|
te.Initialize(config)
|
||||||
te.finishBootstrapping()
|
te.finishBootstrapping()
|
||||||
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(vID ids.ShortID, requestID uint32, vtxID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
}
|
||||||
|
|
||||||
te.PullQuery(vdr.ID(), 0, vtx.ID())
|
te.PullQuery(vdr.ID(), 0, vtx.ID())
|
||||||
te.GetFailed(vdr.ID(), 0, vtx.ID())
|
te.GetFailed(vdr.ID(), *reqID)
|
||||||
|
|
||||||
if len(te.vtxBlocked) != 0 {
|
if len(te.vtxBlocked) != 0 {
|
||||||
t.Fatalf("Should have removed blocking event")
|
t.Fatalf("Should have removed blocking event")
|
||||||
|
@ -2098,7 +2114,7 @@ func TestEngineReissueAbortedVertex(t *testing.T) {
|
||||||
sender.GetF = nil
|
sender.GetF = nil
|
||||||
st.parseVertex = nil
|
st.parseVertex = nil
|
||||||
|
|
||||||
te.GetFailed(vdrID, *requestID, vtxID0)
|
te.GetFailed(vdrID, *requestID)
|
||||||
|
|
||||||
requested := new(bool)
|
requested := new(bool)
|
||||||
sender.GetF = func(_ ids.ShortID, _ uint32, vtxID ids.ID) {
|
sender.GetF = func(_ ids.ShortID, _ uint32, vtxID ids.ID) {
|
||||||
|
@ -2587,3 +2603,375 @@ func TestEngineGossip(t *testing.T) {
|
||||||
t.Fatalf("Should have gossiped the vertex")
|
t.Fatalf("Should have gossiped the vertex")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEngineInvalidVertexIgnoredFromUnexpectedPeer(t *testing.T) {
|
||||||
|
config := DefaultConfig()
|
||||||
|
|
||||||
|
vdr := validators.GenerateRandomValidator(1)
|
||||||
|
secondVdr := validators.GenerateRandomValidator(1)
|
||||||
|
|
||||||
|
vals := validators.NewSet()
|
||||||
|
config.Validators = vals
|
||||||
|
|
||||||
|
vals.Add(vdr)
|
||||||
|
vals.Add(secondVdr)
|
||||||
|
|
||||||
|
sender := &common.SenderTest{}
|
||||||
|
sender.T = t
|
||||||
|
config.Sender = sender
|
||||||
|
|
||||||
|
st := &stateTest{t: t}
|
||||||
|
config.State = st
|
||||||
|
|
||||||
|
gVtx := &Vtx{
|
||||||
|
id: GenerateID(),
|
||||||
|
status: choices.Accepted,
|
||||||
|
bytes: []byte{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
vts := []avalanche.Vertex{gVtx}
|
||||||
|
utxos := []ids.ID{GenerateID(), GenerateID()}
|
||||||
|
|
||||||
|
tx0 := &TestTx{
|
||||||
|
TestTx: snowstorm.TestTx{
|
||||||
|
Identifier: GenerateID(),
|
||||||
|
Stat: choices.Processing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx0.Ins.Add(utxos[0])
|
||||||
|
|
||||||
|
tx1 := &TestTx{
|
||||||
|
TestTx: snowstorm.TestTx{
|
||||||
|
Identifier: GenerateID(),
|
||||||
|
Stat: choices.Processing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx1.Ins.Add(utxos[1])
|
||||||
|
|
||||||
|
vtx0 := &Vtx{
|
||||||
|
parents: vts,
|
||||||
|
id: GenerateID(),
|
||||||
|
txs: []snowstorm.Tx{tx0},
|
||||||
|
height: 1,
|
||||||
|
status: choices.Unknown,
|
||||||
|
bytes: []byte{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
vtx1 := &Vtx{
|
||||||
|
parents: []avalanche.Vertex{vtx0},
|
||||||
|
id: GenerateID(),
|
||||||
|
txs: []snowstorm.Tx{tx1},
|
||||||
|
height: 2,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: []byte{2},
|
||||||
|
}
|
||||||
|
|
||||||
|
te := &Transitive{}
|
||||||
|
te.Initialize(config)
|
||||||
|
te.finishBootstrapping()
|
||||||
|
|
||||||
|
parsed := new(bool)
|
||||||
|
st.parseVertex = func(b []byte) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, vtx1.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return vtx1, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
st.getVertex = func(vtxID ids.ID) (avalanche.Vertex, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtx1.ID()):
|
||||||
|
return vtx1, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(reqVdr ids.ShortID, requestID uint32, vtxID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
if !reqVdr.Equals(vdr.ID()) {
|
||||||
|
t.Fatalf("Wrong validator requested")
|
||||||
|
}
|
||||||
|
if !vtxID.Equals(vtx0.ID()) {
|
||||||
|
t.Fatalf("Wrong vertex requested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
te.PushQuery(vdr.ID(), 0, vtx1.ID(), vtx1.Bytes())
|
||||||
|
|
||||||
|
te.Put(secondVdr.ID(), *reqID, vtx0.ID(), []byte{3})
|
||||||
|
|
||||||
|
*parsed = false
|
||||||
|
st.parseVertex = func(b []byte) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, vtx0.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return vtx0, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
st.getVertex = func(vtxID ids.ID) (avalanche.Vertex, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtx0.ID()):
|
||||||
|
return vtx0, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
sender.CantPushQuery = false
|
||||||
|
sender.CantChits = false
|
||||||
|
|
||||||
|
vtx0.status = choices.Processing
|
||||||
|
|
||||||
|
te.Put(vdr.ID(), *reqID, vtx0.ID(), vtx0.Bytes())
|
||||||
|
|
||||||
|
prefs := te.Consensus.Preferences()
|
||||||
|
if !prefs.Contains(vtx1.ID()) {
|
||||||
|
t.Fatalf("Shouldn't have abandoned the pending vertex")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnginePushQueryRequestIDConflict(t *testing.T) {
|
||||||
|
config := DefaultConfig()
|
||||||
|
|
||||||
|
vdr := validators.GenerateRandomValidator(1)
|
||||||
|
|
||||||
|
vals := validators.NewSet()
|
||||||
|
config.Validators = vals
|
||||||
|
|
||||||
|
vals.Add(vdr)
|
||||||
|
|
||||||
|
sender := &common.SenderTest{}
|
||||||
|
sender.T = t
|
||||||
|
config.Sender = sender
|
||||||
|
|
||||||
|
st := &stateTest{t: t}
|
||||||
|
config.State = st
|
||||||
|
|
||||||
|
gVtx := &Vtx{
|
||||||
|
id: GenerateID(),
|
||||||
|
status: choices.Accepted,
|
||||||
|
bytes: []byte{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
vts := []avalanche.Vertex{gVtx}
|
||||||
|
utxos := []ids.ID{GenerateID(), GenerateID()}
|
||||||
|
|
||||||
|
tx0 := &TestTx{
|
||||||
|
TestTx: snowstorm.TestTx{
|
||||||
|
Identifier: GenerateID(),
|
||||||
|
Stat: choices.Processing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx0.Ins.Add(utxos[0])
|
||||||
|
|
||||||
|
tx1 := &TestTx{
|
||||||
|
TestTx: snowstorm.TestTx{
|
||||||
|
Identifier: GenerateID(),
|
||||||
|
Stat: choices.Processing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx1.Ins.Add(utxos[1])
|
||||||
|
|
||||||
|
vtx0 := &Vtx{
|
||||||
|
parents: vts,
|
||||||
|
id: GenerateID(),
|
||||||
|
txs: []snowstorm.Tx{tx0},
|
||||||
|
height: 1,
|
||||||
|
status: choices.Unknown,
|
||||||
|
bytes: []byte{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
vtx1 := &Vtx{
|
||||||
|
parents: []avalanche.Vertex{vtx0},
|
||||||
|
id: GenerateID(),
|
||||||
|
txs: []snowstorm.Tx{tx1},
|
||||||
|
height: 2,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: []byte{2},
|
||||||
|
}
|
||||||
|
|
||||||
|
randomVtxID := GenerateID()
|
||||||
|
|
||||||
|
te := &Transitive{}
|
||||||
|
te.Initialize(config)
|
||||||
|
te.finishBootstrapping()
|
||||||
|
|
||||||
|
parsed := new(bool)
|
||||||
|
st.parseVertex = func(b []byte) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, vtx1.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return vtx1, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
st.getVertex = func(vtxID ids.ID) (avalanche.Vertex, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtx1.ID()):
|
||||||
|
return vtx1, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(reqVdr ids.ShortID, requestID uint32, vtxID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
if !reqVdr.Equals(vdr.ID()) {
|
||||||
|
t.Fatalf("Wrong validator requested")
|
||||||
|
}
|
||||||
|
if !vtxID.Equals(vtx0.ID()) {
|
||||||
|
t.Fatalf("Wrong vertex requested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
te.PushQuery(vdr.ID(), 0, vtx1.ID(), vtx1.Bytes())
|
||||||
|
|
||||||
|
sender.GetF = nil
|
||||||
|
sender.CantGet = false
|
||||||
|
|
||||||
|
te.PushQuery(vdr.ID(), *reqID, randomVtxID, []byte{3})
|
||||||
|
|
||||||
|
*parsed = false
|
||||||
|
st.parseVertex = func(b []byte) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, vtx0.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return vtx0, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
st.getVertex = func(vtxID ids.ID) (avalanche.Vertex, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtx0.ID()):
|
||||||
|
return vtx0, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
sender.CantPushQuery = false
|
||||||
|
sender.CantChits = false
|
||||||
|
|
||||||
|
vtx0.status = choices.Processing
|
||||||
|
|
||||||
|
te.Put(vdr.ID(), *reqID, vtx0.ID(), vtx0.Bytes())
|
||||||
|
|
||||||
|
prefs := te.Consensus.Preferences()
|
||||||
|
if !prefs.Contains(vtx1.ID()) {
|
||||||
|
t.Fatalf("Shouldn't have abandoned the pending vertex")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngineAggressivePolling(t *testing.T) {
|
||||||
|
config := DefaultConfig()
|
||||||
|
|
||||||
|
config.Params.ConcurrentRepolls = 3
|
||||||
|
|
||||||
|
vdr := validators.GenerateRandomValidator(1)
|
||||||
|
|
||||||
|
vals := validators.NewSet()
|
||||||
|
config.Validators = vals
|
||||||
|
|
||||||
|
vals.Add(vdr)
|
||||||
|
|
||||||
|
sender := &common.SenderTest{}
|
||||||
|
sender.T = t
|
||||||
|
config.Sender = sender
|
||||||
|
|
||||||
|
st := &stateTest{t: t}
|
||||||
|
config.State = st
|
||||||
|
|
||||||
|
gVtx := &Vtx{
|
||||||
|
id: GenerateID(),
|
||||||
|
status: choices.Accepted,
|
||||||
|
bytes: []byte{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
vts := []avalanche.Vertex{gVtx}
|
||||||
|
utxos := []ids.ID{GenerateID(), GenerateID()}
|
||||||
|
|
||||||
|
tx0 := &TestTx{
|
||||||
|
TestTx: snowstorm.TestTx{
|
||||||
|
Identifier: GenerateID(),
|
||||||
|
Stat: choices.Processing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx0.Ins.Add(utxos[0])
|
||||||
|
|
||||||
|
tx1 := &TestTx{
|
||||||
|
TestTx: snowstorm.TestTx{
|
||||||
|
Identifier: GenerateID(),
|
||||||
|
Stat: choices.Processing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tx1.Ins.Add(utxos[1])
|
||||||
|
|
||||||
|
vtx := &Vtx{
|
||||||
|
parents: vts,
|
||||||
|
id: GenerateID(),
|
||||||
|
txs: []snowstorm.Tx{tx0},
|
||||||
|
height: 1,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: []byte{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
te := &Transitive{}
|
||||||
|
te.Initialize(config)
|
||||||
|
te.finishBootstrapping()
|
||||||
|
|
||||||
|
parsed := new(bool)
|
||||||
|
st.parseVertex = func(b []byte) (avalanche.Vertex, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, vtx.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return vtx, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
st.getVertex = func(vtxID ids.ID) (avalanche.Vertex, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(vtx.ID()):
|
||||||
|
return vtx, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownVertex
|
||||||
|
}
|
||||||
|
|
||||||
|
numPushQueries := new(int)
|
||||||
|
sender.PushQueryF = func(ids.ShortSet, uint32, ids.ID, []byte) { *numPushQueries++ }
|
||||||
|
|
||||||
|
numPullQueries := new(int)
|
||||||
|
sender.PullQueryF = func(ids.ShortSet, uint32, ids.ID) { *numPullQueries++ }
|
||||||
|
|
||||||
|
te.Put(vdr.ID(), 0, vtx.ID(), vtx.Bytes())
|
||||||
|
|
||||||
|
if *numPushQueries != 1 {
|
||||||
|
t.Fatalf("should have issued one push query")
|
||||||
|
}
|
||||||
|
if *numPullQueries != 2 {
|
||||||
|
t.Fatalf("should have issued one pull query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -59,11 +59,8 @@ func (v *voter) Update() {
|
||||||
}
|
}
|
||||||
|
|
||||||
v.t.Config.Context.Log.Verbo("Avalanche engine can't quiesce")
|
v.t.Config.Context.Log.Verbo("Avalanche engine can't quiesce")
|
||||||
|
|
||||||
if len(v.t.polls.m) < v.t.Config.Params.ConcurrentRepolls {
|
|
||||||
v.t.repoll()
|
v.t.repoll()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (v *voter) bubbleVotes(votes ids.UniqueBag) ids.UniqueBag {
|
func (v *voter) bubbleVotes(votes ids.UniqueBag) ids.UniqueBag {
|
||||||
bubbledVotes := ids.UniqueBag{}
|
bubbledVotes := ids.UniqueBag{}
|
||||||
|
|
|
@ -34,75 +34,181 @@ type ExternalHandler interface {
|
||||||
// FrontierHandler defines how a consensus engine reacts to frontier messages
|
// FrontierHandler defines how a consensus engine reacts to frontier messages
|
||||||
// from other validators
|
// from other validators
|
||||||
type FrontierHandler interface {
|
type FrontierHandler interface {
|
||||||
// GetAcceptedFrontier notifies this consensus engine that its accepted
|
// Notify this engine of a request for the accepted frontier of vertices.
|
||||||
// frontier is requested by the specified validator
|
//
|
||||||
|
// The accepted frontier is the set of accepted vertices that do not have
|
||||||
|
// any accepted descendants.
|
||||||
|
//
|
||||||
|
// This function can be called by any validator. It is not safe to assume
|
||||||
|
// this message is utilizing a unique requestID. However, the validatorID is
|
||||||
|
// assumed to be authenticated.
|
||||||
|
//
|
||||||
|
// This engine should respond with an AcceptedFrontier message with the same
|
||||||
|
// requestID, and the engine's current accepted frontier.
|
||||||
GetAcceptedFrontier(validatorID ids.ShortID, requestID uint32)
|
GetAcceptedFrontier(validatorID ids.ShortID, requestID uint32)
|
||||||
|
|
||||||
// AcceptedFrontier notifies this consensus engine of the specified
|
// Notify this engine of an accepted frontier.
|
||||||
// validators current accepted frontier
|
//
|
||||||
AcceptedFrontier(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
// This function can be called by any validator. It is not safe to assume
|
||||||
|
// this message is in response to a GetAcceptedFrontier message, is utilizing a
|
||||||
|
// unique requestID, or that the containerIDs from a valid frontier.
|
||||||
|
// However, the validatorID is assumed to be authenticated.
|
||||||
|
AcceptedFrontier(
|
||||||
|
validatorID ids.ShortID,
|
||||||
|
requestID uint32,
|
||||||
|
containerIDs ids.Set,
|
||||||
|
)
|
||||||
|
|
||||||
// GetAcceptedFrontierFailed notifies this consensus engine that the
|
// Notify this engine that a get accepted frontier request it issued has
|
||||||
// requested accepted frontier from the specified validator should be
|
// failed.
|
||||||
// considered lost
|
//
|
||||||
|
// This function will be called if the engine sent a GetAcceptedFrontier
|
||||||
|
// message that is not anticipated to be responded to. This could be because
|
||||||
|
// the recipient of the message is unknown or if the message request has
|
||||||
|
// timed out.
|
||||||
|
//
|
||||||
|
// The validatorID, and requestID, are assumed to be the same as those sent
|
||||||
|
// in the GetAcceptedFrontier message.
|
||||||
GetAcceptedFrontierFailed(validatorID ids.ShortID, requestID uint32)
|
GetAcceptedFrontierFailed(validatorID ids.ShortID, requestID uint32)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptedHandler defines how a consensus engine reacts to messages pertaining
|
// AcceptedHandler defines how a consensus engine reacts to messages pertaining
|
||||||
// to accepted containers from other validators
|
// to accepted containers from other validators
|
||||||
type AcceptedHandler interface {
|
type AcceptedHandler interface {
|
||||||
// GetAccepted notifies this consensus engine that it should send the set of
|
// Notify this engine of a request to filter non-accepted vertices.
|
||||||
// containerIDs that it has accepted from the provided set to the specified
|
//
|
||||||
// validator
|
// This function can be called by any validator. It is not safe to assume
|
||||||
|
// this message is utilizing a unique requestID. However, the validatorID is
|
||||||
|
// assumed to be authenticated.
|
||||||
|
//
|
||||||
|
// This engine should respond with an Accepted message with the same
|
||||||
|
// requestID, and the subset of the containerIDs that this node has decided
|
||||||
|
// are accepted.
|
||||||
GetAccepted(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
GetAccepted(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
||||||
|
|
||||||
// Accepted notifies this consensus engine of a set of accepted containerIDs
|
// Notify this engine of a set of accepted vertices.
|
||||||
|
//
|
||||||
|
// This function can be called by any validator. It is not safe to assume
|
||||||
|
// this message is in response to a GetAccepted message, is utilizing a
|
||||||
|
// unique requestID, or that the containerIDs are a subset of the
|
||||||
|
// containerIDs from a GetAccepted message. However, the validatorID is
|
||||||
|
// assumed to be authenticated.
|
||||||
Accepted(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
Accepted(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
||||||
|
|
||||||
// GetAcceptedFailed notifies this consensus engine that the requested
|
// Notify this engine that a get accepted request it issued has failed.
|
||||||
// accepted containers requested from the specified validator should be
|
//
|
||||||
// considered lost
|
// This function will be called if the engine sent a GetAccepted message
|
||||||
|
// that is not anticipated to be responded to. This could be because the
|
||||||
|
// recipient of the message is unknown or if the message request has timed
|
||||||
|
// out.
|
||||||
|
//
|
||||||
|
// The validatorID, and requestID, are assumed to be the same as those sent
|
||||||
|
// in the GetAccepted message.
|
||||||
GetAcceptedFailed(validatorID ids.ShortID, requestID uint32)
|
GetAcceptedFailed(validatorID ids.ShortID, requestID uint32)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchHandler defines how a consensus engine reacts to retrieval messages from
|
// FetchHandler defines how a consensus engine reacts to retrieval messages from
|
||||||
// other validators
|
// other validators
|
||||||
type FetchHandler interface {
|
type FetchHandler interface {
|
||||||
// Get notifies this consensus engine that the specified validator requested
|
// Notify this engine of a request for a container.
|
||||||
// that this engine send the specified container to it
|
//
|
||||||
|
// This function can be called by any validator. It is not safe to assume
|
||||||
|
// this message is utilizing a unique requestID. It is also not safe to
|
||||||
|
// assume the requested containerID exists. However, the validatorID is
|
||||||
|
// assumed to be authenticated.
|
||||||
|
//
|
||||||
|
// There should never be a situation where a virtuous node sends a Get
|
||||||
|
// request to another virtuous node that does not have the requested
|
||||||
|
// container. Unless that container was pruned from the active set.
|
||||||
|
//
|
||||||
|
// This engine should respond with a Put message with the same requestID if
|
||||||
|
// the container was locally avaliable. Otherwise, the message can be safely
|
||||||
|
// dropped.
|
||||||
Get(validatorID ids.ShortID, requestID uint32, containerID ids.ID)
|
Get(validatorID ids.ShortID, requestID uint32, containerID ids.ID)
|
||||||
|
|
||||||
// Put the container with the specified ID and body.
|
// Notify this engine of a container.
|
||||||
|
//
|
||||||
|
// This function can be called by any validator. It is not safe to assume
|
||||||
|
// this message is utilizing a unique requestID or even that the containerID
|
||||||
|
// matches the ID of the container bytes. However, the validatorID is
|
||||||
|
// assumed to be authenticated.
|
||||||
|
//
|
||||||
// This engine needs to request and receive missing ancestors of the
|
// This engine needs to request and receive missing ancestors of the
|
||||||
// container before adding the container to consensus. Once all ancestor
|
// container before adding the container to consensus. Once all ancestor
|
||||||
// containers are added, pushes the container into the consensus.
|
// containers are added, pushes the container into the consensus.
|
||||||
Put(validatorID ids.ShortID, requestID uint32, containerID ids.ID, container []byte)
|
Put(
|
||||||
|
validatorID ids.ShortID,
|
||||||
|
requestID uint32,
|
||||||
|
containerID ids.ID,
|
||||||
|
container []byte,
|
||||||
|
)
|
||||||
|
|
||||||
// Notify this engine that a get request it issued has failed.
|
// Notify this engine that a get request it issued has failed.
|
||||||
GetFailed(validatorID ids.ShortID, requestID uint32, containerID ids.ID)
|
//
|
||||||
|
// This function will be called if the engine sent a Get message that is not
|
||||||
|
// anticipated to be responded to. This could be because the recipient of
|
||||||
|
// the message is unknown or if the message request has timed out.
|
||||||
|
//
|
||||||
|
// The validatorID and requestID are assumed to be the same as those sent in
|
||||||
|
// the Get message.
|
||||||
|
GetFailed(validatorID ids.ShortID, requestID uint32)
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryHandler defines how a consensus engine reacts to query messages from
|
// QueryHandler defines how a consensus engine reacts to query messages from
|
||||||
// other validators
|
// other validators
|
||||||
type QueryHandler interface {
|
type QueryHandler interface {
|
||||||
// Notify this engine that the specified validator queried it about the
|
// Notify this engine of a request for our preferences.
|
||||||
// specified container. That is, the validator would like to know whether
|
//
|
||||||
// this engine prefers the specified container. If the ancestry of the
|
// This function can be called by any validator. It is not safe to assume
|
||||||
// container is incomplete, or the container is unknown, request the missing
|
// this message is utilizing a unique requestID. However, the validatorID is
|
||||||
// data. Once complete, sends this validator the current preferences.
|
// assumed to be authenticated.
|
||||||
|
//
|
||||||
|
// If the container or its ancestry is incomplete, this engine is expected
|
||||||
|
// to request the missing containers from the validator. Once the ancestry
|
||||||
|
// is complete, this engine should send this validator the current
|
||||||
|
// preferences in a Chits message. The Chits message should have the same
|
||||||
|
// requestID that was passed in here.
|
||||||
PullQuery(validatorID ids.ShortID, requestID uint32, containerID ids.ID)
|
PullQuery(validatorID ids.ShortID, requestID uint32, containerID ids.ID)
|
||||||
|
|
||||||
// Notify this engine that the specified validator queried it about the
|
// Notify this engine of a request for our preferences.
|
||||||
// specified container. That is, the validator would like to know whether
|
//
|
||||||
// this engine prefers the specified container. If the ancestry of the
|
// This function can be called by any validator. It is not safe to assume
|
||||||
// container is incomplete, request it. Once complete, sends this validator
|
// this message is utilizing a unique requestID or even that the containerID
|
||||||
// the current preferences.
|
// matches the ID of the container bytes. However, the validatorID is
|
||||||
PushQuery(validatorID ids.ShortID, requestID uint32, containerID ids.ID, container []byte)
|
// assumed to be authenticated.
|
||||||
|
//
|
||||||
|
// This function is meant to behave the same way as PullQuery, except the
|
||||||
|
// container is optimistically provided to potentially remove the need for
|
||||||
|
// a series of Get/Put messages.
|
||||||
|
//
|
||||||
|
// If the ancestry of the container is incomplete, this engine is expected
|
||||||
|
// to request the ancestry from the validator. Once the ancestry is
|
||||||
|
// complete, this engine should send this validator the current preferences
|
||||||
|
// in a Chits message. The Chits message should have the same requestID that
|
||||||
|
// was passed in here.
|
||||||
|
PushQuery(
|
||||||
|
validatorID ids.ShortID,
|
||||||
|
requestID uint32,
|
||||||
|
containerID ids.ID,
|
||||||
|
container []byte,
|
||||||
|
)
|
||||||
|
|
||||||
// Notify this engine of the specified validators preferences.
|
// Notify this engine of the specified validators preferences.
|
||||||
|
//
|
||||||
|
// This function can be called by any validator. It is not safe to assume
|
||||||
|
// this message is in response to a PullQuery or a PushQuery message.
|
||||||
|
// However, the validatorID is assumed to be authenticated.
|
||||||
Chits(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
Chits(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
||||||
|
|
||||||
// Notify this engine that a query it issued has failed.
|
// Notify this engine that a query it issued has failed.
|
||||||
|
//
|
||||||
|
// This function will be called if the engine sent a PullQuery or PushQuery
|
||||||
|
// message that is not anticipated to be responded to. This could be because
|
||||||
|
// the recipient of the message is unknown or if the message request has
|
||||||
|
// timed out.
|
||||||
|
//
|
||||||
|
// The validatorID and the requestID are assumed to be the same as those
|
||||||
|
// sent in the Query message.
|
||||||
QueryFailed(validatorID ids.ShortID, requestID uint32)
|
QueryFailed(validatorID ids.ShortID, requestID uint32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,14 +216,19 @@ type QueryHandler interface {
|
||||||
// other components of this validator
|
// other components of this validator
|
||||||
type InternalHandler interface {
|
type InternalHandler interface {
|
||||||
// Startup this engine.
|
// Startup this engine.
|
||||||
|
//
|
||||||
|
// This function will be called once the environment is configured to be
|
||||||
|
// able to run the engine.
|
||||||
Startup()
|
Startup()
|
||||||
|
|
||||||
// Gossip to the network a container on the accepted frontier
|
// Gossip to the network a container on the accepted frontier
|
||||||
Gossip()
|
Gossip()
|
||||||
|
|
||||||
// Shutdown this engine.
|
// Shutdown this engine.
|
||||||
|
//
|
||||||
|
// This function will be called when the environment is exiting.
|
||||||
Shutdown()
|
Shutdown()
|
||||||
|
|
||||||
// Notify this engine that the vm has sent a message to it.
|
// Notify this engine of a message from the virtual machine.
|
||||||
Notify(Message)
|
Notify(Message)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
|
||||||
|
// See the file LICENSE for licensing terms.
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ava-labs/gecko/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
type req struct {
|
||||||
|
vdr ids.ShortID
|
||||||
|
id uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requests tracks pending container messages from a peer.
|
||||||
|
type Requests struct {
|
||||||
|
reqsToID map[[20]byte]map[uint32]ids.ID
|
||||||
|
idToReq map[[32]byte]req
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a request. Assumes that requestIDs are unique. Assumes that containerIDs
|
||||||
|
// are only in one request at a time.
|
||||||
|
func (r *Requests) Add(vdr ids.ShortID, requestID uint32, containerID ids.ID) {
|
||||||
|
if r.reqsToID == nil {
|
||||||
|
r.reqsToID = make(map[[20]byte]map[uint32]ids.ID)
|
||||||
|
}
|
||||||
|
vdrKey := vdr.Key()
|
||||||
|
vdrReqs, ok := r.reqsToID[vdrKey]
|
||||||
|
if !ok {
|
||||||
|
vdrReqs = make(map[uint32]ids.ID)
|
||||||
|
r.reqsToID[vdrKey] = vdrReqs
|
||||||
|
}
|
||||||
|
vdrReqs[requestID] = containerID
|
||||||
|
|
||||||
|
if r.idToReq == nil {
|
||||||
|
r.idToReq = make(map[[32]byte]req)
|
||||||
|
}
|
||||||
|
r.idToReq[containerID.Key()] = req{
|
||||||
|
vdr: vdr,
|
||||||
|
id: requestID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove attempts to abandon a requestID sent to a validator. If the request is
|
||||||
|
// currently outstanding, the requested ID will be returned along with true. If
|
||||||
|
// the request isn't currently outstanding, false will be returned.
|
||||||
|
func (r *Requests) Remove(vdr ids.ShortID, requestID uint32) (ids.ID, bool) {
|
||||||
|
vdrKey := vdr.Key()
|
||||||
|
vdrReqs, ok := r.reqsToID[vdrKey]
|
||||||
|
if !ok {
|
||||||
|
return ids.ID{}, false
|
||||||
|
}
|
||||||
|
containerID, ok := vdrReqs[requestID]
|
||||||
|
if !ok {
|
||||||
|
return ids.ID{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vdrReqs) == 1 {
|
||||||
|
delete(r.reqsToID, vdrKey)
|
||||||
|
} else {
|
||||||
|
delete(vdrReqs, requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(r.idToReq, containerID.Key())
|
||||||
|
return containerID, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAny outstanding requests for the container ID. True is returned if the
|
||||||
|
// container ID had an outstanding request.
|
||||||
|
func (r *Requests) RemoveAny(containerID ids.ID) bool {
|
||||||
|
req, ok := r.idToReq[containerID.Key()]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Remove(req.vdr, req.id)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the total number of outstanding requests.
|
||||||
|
func (r *Requests) Len() int { return len(r.idToReq) }
|
||||||
|
|
||||||
|
// Contains returns true if there is an outstanding request for the container
|
||||||
|
// ID.
|
||||||
|
func (r *Requests) Contains(containerID ids.ID) bool {
|
||||||
|
_, ok := r.idToReq[containerID.Key()]
|
||||||
|
return ok
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
|
||||||
|
// See the file LICENSE for licensing terms.
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/ava-labs/gecko/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequests(t *testing.T) {
|
||||||
|
req := Requests{}
|
||||||
|
|
||||||
|
length := req.Len()
|
||||||
|
assert.Equal(t, 0, length, "should have had no outstanding requests")
|
||||||
|
|
||||||
|
_, removed := req.Remove(ids.ShortEmpty, 0)
|
||||||
|
assert.False(t, removed, "shouldn't have removed the request")
|
||||||
|
|
||||||
|
removed = req.RemoveAny(ids.Empty)
|
||||||
|
assert.False(t, removed, "shouldn't have removed the request")
|
||||||
|
|
||||||
|
constains := req.Contains(ids.Empty)
|
||||||
|
assert.False(t, constains, "shouldn't contain this request")
|
||||||
|
|
||||||
|
req.Add(ids.ShortEmpty, 0, ids.Empty)
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 1, length, "should have had one outstanding request")
|
||||||
|
|
||||||
|
_, removed = req.Remove(ids.ShortEmpty, 1)
|
||||||
|
assert.False(t, removed, "shouldn't have removed the request")
|
||||||
|
|
||||||
|
_, removed = req.Remove(ids.NewShortID([20]byte{1}), 0)
|
||||||
|
assert.False(t, removed, "shouldn't have removed the request")
|
||||||
|
|
||||||
|
constains = req.Contains(ids.Empty)
|
||||||
|
assert.True(t, constains, "should contain this request")
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 1, length, "should have had one outstanding request")
|
||||||
|
|
||||||
|
req.Add(ids.ShortEmpty, 10, ids.Empty.Prefix(0))
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 2, length, "should have had two outstanding requests")
|
||||||
|
|
||||||
|
_, removed = req.Remove(ids.ShortEmpty, 1)
|
||||||
|
assert.False(t, removed, "shouldn't have removed the request")
|
||||||
|
|
||||||
|
_, removed = req.Remove(ids.NewShortID([20]byte{1}), 0)
|
||||||
|
assert.False(t, removed, "shouldn't have removed the request")
|
||||||
|
|
||||||
|
constains = req.Contains(ids.Empty)
|
||||||
|
assert.True(t, constains, "should contain this request")
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 2, length, "should have had two outstanding requests")
|
||||||
|
|
||||||
|
removedID, removed := req.Remove(ids.ShortEmpty, 0)
|
||||||
|
assert.True(t, removedID.Equals(ids.Empty), "should have removed the requested ID")
|
||||||
|
assert.True(t, removed, "should have removed the request")
|
||||||
|
|
||||||
|
removedID, removed = req.Remove(ids.ShortEmpty, 10)
|
||||||
|
assert.True(t, removedID.Equals(ids.Empty.Prefix(0)), "should have removed the requested ID")
|
||||||
|
assert.True(t, removed, "should have removed the request")
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 0, length, "should have had no outstanding requests")
|
||||||
|
|
||||||
|
req.Add(ids.ShortEmpty, 0, ids.Empty)
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 1, length, "should have had one outstanding request")
|
||||||
|
|
||||||
|
removed = req.RemoveAny(ids.Empty)
|
||||||
|
assert.True(t, removed, "should have removed the request")
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 0, length, "should have had no outstanding requests")
|
||||||
|
|
||||||
|
removed = req.RemoveAny(ids.Empty)
|
||||||
|
assert.False(t, removed, "shouldn't have removed the request")
|
||||||
|
|
||||||
|
length = req.Len()
|
||||||
|
assert.Equal(t, 0, length, "should have had no outstanding requests")
|
||||||
|
}
|
|
@ -42,7 +42,8 @@ type EngineTest struct {
|
||||||
StartupF, GossipF, ShutdownF func()
|
StartupF, GossipF, ShutdownF func()
|
||||||
ContextF func() *snow.Context
|
ContextF func() *snow.Context
|
||||||
NotifyF func(Message)
|
NotifyF func(Message)
|
||||||
GetF, GetFailedF, PullQueryF func(validatorID ids.ShortID, requestID uint32, containerID ids.ID)
|
GetF, PullQueryF func(validatorID ids.ShortID, requestID uint32, containerID ids.ID)
|
||||||
|
GetFailedF func(validatorID ids.ShortID, requestID uint32)
|
||||||
PutF, PushQueryF func(validatorID ids.ShortID, requestID uint32, containerID ids.ID, container []byte)
|
PutF, PushQueryF func(validatorID ids.ShortID, requestID uint32, containerID ids.ID, container []byte)
|
||||||
GetAcceptedFrontierF, GetAcceptedFrontierFailedF, GetAcceptedFailedF, QueryFailedF func(validatorID ids.ShortID, requestID uint32)
|
GetAcceptedFrontierF, GetAcceptedFrontierFailedF, GetAcceptedFailedF, QueryFailedF func(validatorID ids.ShortID, requestID uint32)
|
||||||
AcceptedFrontierF, GetAcceptedF, AcceptedF, ChitsF func(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
AcceptedFrontierF, GetAcceptedF, AcceptedF, ChitsF func(validatorID ids.ShortID, requestID uint32, containerIDs ids.Set)
|
||||||
|
@ -187,9 +188,9 @@ func (e *EngineTest) Get(validatorID ids.ShortID, requestID uint32, containerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFailed ...
|
// GetFailed ...
|
||||||
func (e *EngineTest) GetFailed(validatorID ids.ShortID, requestID uint32, containerID ids.ID) {
|
func (e *EngineTest) GetFailed(validatorID ids.ShortID, requestID uint32) {
|
||||||
if e.GetFailedF != nil {
|
if e.GetFailedF != nil {
|
||||||
e.GetFailedF(validatorID, requestID, containerID)
|
e.GetFailedF(validatorID, requestID)
|
||||||
} else if e.CantGetFailed && e.T != nil {
|
} else if e.CantGetFailed && e.T != nil {
|
||||||
e.T.Fatalf("Unexpectedly called GetFailed")
|
e.T.Fatalf("Unexpectedly called GetFailed")
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,9 @@ type BootstrapConfig struct {
|
||||||
// Blocked tracks operations that are blocked on blocks
|
// Blocked tracks operations that are blocked on blocks
|
||||||
Blocked *queue.Jobs
|
Blocked *queue.Jobs
|
||||||
|
|
||||||
|
// blocks that have outstanding get requests
|
||||||
|
blkReqs common.Requests
|
||||||
|
|
||||||
VM ChainVM
|
VM ChainVM
|
||||||
|
|
||||||
Bootstrapped func()
|
Bootstrapped func()
|
||||||
|
@ -84,16 +87,22 @@ func (b *bootstrapper) ForceAccepted(acceptedContainerIDs ids.Set) {
|
||||||
func (b *bootstrapper) Put(vdr ids.ShortID, requestID uint32, blkID ids.ID, blkBytes []byte) {
|
func (b *bootstrapper) Put(vdr ids.ShortID, requestID uint32, blkID ids.ID, blkBytes []byte) {
|
||||||
b.BootstrapConfig.Context.Log.Verbo("Put called for blkID %s", blkID)
|
b.BootstrapConfig.Context.Log.Verbo("Put called for blkID %s", blkID)
|
||||||
|
|
||||||
if !b.pending.Contains(blkID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
blk, err := b.VM.ParseBlock(blkBytes)
|
blk, err := b.VM.ParseBlock(blkBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.BootstrapConfig.Context.Log.Debug("ParseBlock failed due to %s for block:\n%s",
|
b.BootstrapConfig.Context.Log.Debug("ParseBlock failed due to %s for block:\n%s",
|
||||||
err,
|
err,
|
||||||
formatting.DumpBytes{Bytes: blkBytes})
|
formatting.DumpBytes{Bytes: blkBytes})
|
||||||
b.GetFailed(vdr, requestID, blkID)
|
|
||||||
|
b.GetFailed(vdr, requestID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.pending.Contains(blk.ID()) {
|
||||||
|
b.BootstrapConfig.Context.Log.Debug("Validator %s sent an unrequested block:\n%s",
|
||||||
|
vdr,
|
||||||
|
formatting.DumpBytes{Bytes: blkBytes})
|
||||||
|
|
||||||
|
b.GetFailed(vdr, requestID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +110,15 @@ func (b *bootstrapper) Put(vdr ids.ShortID, requestID uint32, blkID ids.ID, blkB
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFailed ...
|
// GetFailed ...
|
||||||
func (b *bootstrapper) GetFailed(_ ids.ShortID, _ uint32, blkID ids.ID) { b.sendRequest(blkID) }
|
func (b *bootstrapper) GetFailed(vdr ids.ShortID, requestID uint32) {
|
||||||
|
blkID, ok := b.blkReqs.Remove(vdr, requestID)
|
||||||
|
if !ok {
|
||||||
|
b.BootstrapConfig.Context.Log.Debug("GetFailed called without sending the corresponding Get message from %s",
|
||||||
|
vdr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.sendRequest(blkID)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *bootstrapper) fetch(blkID ids.ID) {
|
func (b *bootstrapper) fetch(blkID ids.ID) {
|
||||||
if b.pending.Contains(blkID) {
|
if b.pending.Contains(blkID) {
|
||||||
|
@ -125,6 +142,9 @@ func (b *bootstrapper) sendRequest(blkID ids.ID) {
|
||||||
validatorID := validators[0].ID()
|
validatorID := validators[0].ID()
|
||||||
b.RequestID++
|
b.RequestID++
|
||||||
|
|
||||||
|
b.blkReqs.RemoveAny(blkID)
|
||||||
|
b.blkReqs.Add(validatorID, b.RequestID, blkID)
|
||||||
|
|
||||||
b.pending.Add(blkID)
|
b.pending.Add(blkID)
|
||||||
b.BootstrapConfig.Sender.Get(validatorID, b.RequestID, blkID)
|
b.BootstrapConfig.Sender.Get(validatorID, b.RequestID, blkID)
|
||||||
|
|
||||||
|
|
|
@ -223,12 +223,13 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) {
|
||||||
bs.ForceAccepted(acceptedIDs)
|
bs.ForceAccepted(acceptedIDs)
|
||||||
|
|
||||||
vm.GetBlockF = nil
|
vm.GetBlockF = nil
|
||||||
sender.GetF = nil
|
|
||||||
|
|
||||||
vm.ParseBlockF = func(blkBytes []byte) (snowman.Block, error) {
|
vm.ParseBlockF = func(blkBytes []byte) (snowman.Block, error) {
|
||||||
switch {
|
switch {
|
||||||
case bytes.Equal(blkBytes, blkBytes1):
|
case bytes.Equal(blkBytes, blkBytes1):
|
||||||
return blk1, nil
|
return blk1, nil
|
||||||
|
case bytes.Equal(blkBytes, blkBytes2):
|
||||||
|
return blk2, nil
|
||||||
}
|
}
|
||||||
t.Fatal(errUnknownBlock)
|
t.Fatal(errUnknownBlock)
|
||||||
return nil, errUnknownBlock
|
return nil, errUnknownBlock
|
||||||
|
@ -477,3 +478,113 @@ func TestBootstrapperPartialFetch(t *testing.T) {
|
||||||
t.Fatalf("wrong number pending")
|
t.Fatalf("wrong number pending")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBootstrapperWrongIDByzantineResponse(t *testing.T) {
|
||||||
|
config, peerID, sender, vm := newConfig(t)
|
||||||
|
|
||||||
|
blkID0 := ids.Empty.Prefix(0)
|
||||||
|
blkID1 := ids.Empty.Prefix(1)
|
||||||
|
blkID2 := ids.Empty.Prefix(2)
|
||||||
|
|
||||||
|
blkBytes0 := []byte{0}
|
||||||
|
blkBytes1 := []byte{1}
|
||||||
|
blkBytes2 := []byte{2}
|
||||||
|
|
||||||
|
blk0 := &Blk{
|
||||||
|
id: blkID0,
|
||||||
|
height: 0,
|
||||||
|
status: choices.Accepted,
|
||||||
|
bytes: blkBytes0,
|
||||||
|
}
|
||||||
|
blk1 := &Blk{
|
||||||
|
parent: blk0,
|
||||||
|
id: blkID1,
|
||||||
|
height: 1,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: blkBytes1,
|
||||||
|
}
|
||||||
|
blk2 := &Blk{
|
||||||
|
parent: blk1,
|
||||||
|
id: blkID2,
|
||||||
|
height: 2,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: blkBytes2,
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := bootstrapper{}
|
||||||
|
bs.metrics.Initialize(config.Context.Log, fmt.Sprintf("gecko_%s", config.Context.ChainID), prometheus.NewRegistry())
|
||||||
|
bs.Initialize(config)
|
||||||
|
|
||||||
|
acceptedIDs := ids.Set{}
|
||||||
|
acceptedIDs.Add(blkID1)
|
||||||
|
|
||||||
|
vm.GetBlockF = func(blkID ids.ID) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case blkID.Equals(blkID1):
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
default:
|
||||||
|
t.Fatal(errUnknownBlock)
|
||||||
|
panic(errUnknownBlock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestID := new(uint32)
|
||||||
|
sender.GetF = func(vdr ids.ShortID, reqID uint32, vtxID ids.ID) {
|
||||||
|
if !vdr.Equals(peerID) {
|
||||||
|
t.Fatalf("Should have requested block from %s, requested from %s", peerID, vdr)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case vtxID.Equals(blkID1):
|
||||||
|
default:
|
||||||
|
t.Fatalf("Requested unknown block")
|
||||||
|
}
|
||||||
|
|
||||||
|
*requestID = reqID
|
||||||
|
}
|
||||||
|
|
||||||
|
bs.ForceAccepted(acceptedIDs)
|
||||||
|
|
||||||
|
vm.GetBlockF = nil
|
||||||
|
sender.GetF = nil
|
||||||
|
|
||||||
|
vm.ParseBlockF = func(blkBytes []byte) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(blkBytes, blkBytes2):
|
||||||
|
return blk2, nil
|
||||||
|
}
|
||||||
|
t.Fatal(errUnknownBlock)
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.CantGet = false
|
||||||
|
|
||||||
|
bs.Put(peerID, *requestID, blkID1, blkBytes2)
|
||||||
|
|
||||||
|
sender.CantGet = true
|
||||||
|
|
||||||
|
vm.ParseBlockF = func(blkBytes []byte) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(blkBytes, blkBytes1):
|
||||||
|
return blk1, nil
|
||||||
|
}
|
||||||
|
t.Fatal(errUnknownBlock)
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
finished := new(bool)
|
||||||
|
bs.onFinished = func() { *finished = true }
|
||||||
|
|
||||||
|
bs.Put(peerID, *requestID, blkID1, blkBytes1)
|
||||||
|
|
||||||
|
vm.ParseBlockF = nil
|
||||||
|
|
||||||
|
if !*finished {
|
||||||
|
t.Fatalf("Bootstrapping should have finished")
|
||||||
|
}
|
||||||
|
if blk1.Status() != choices.Accepted {
|
||||||
|
t.Fatalf("Block should be accepted")
|
||||||
|
}
|
||||||
|
if blk2.Status() != choices.Processing {
|
||||||
|
t.Fatalf("Block should be processing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,12 +19,22 @@ type Transitive struct {
|
||||||
Config
|
Config
|
||||||
bootstrapper
|
bootstrapper
|
||||||
|
|
||||||
polls polls // track people I have asked for their preference
|
// track outstanding preference requests
|
||||||
|
polls polls
|
||||||
|
|
||||||
blkReqs, pending ids.Set // prevent asking validators for the same block
|
// blocks that have outstanding get requests
|
||||||
|
blkReqs common.Requests
|
||||||
|
|
||||||
blocked events.Blocker // track operations that are blocked on blocks
|
// blocks that are fetched but haven't been issued due to missing
|
||||||
|
// dependencies
|
||||||
|
pending ids.Set
|
||||||
|
|
||||||
|
// operations that are blocked on a block being issued. This could be
|
||||||
|
// issuing another block, responding to a query, or applying votes to
|
||||||
|
// consensus
|
||||||
|
blocked events.Blocker
|
||||||
|
|
||||||
|
// mark for if the engine has been bootstrapped or not
|
||||||
bootstrapped bool
|
bootstrapped bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +43,11 @@ func (t *Transitive) Initialize(config Config) {
|
||||||
config.Context.Log.Info("Initializing Snowman consensus")
|
config.Context.Log.Info("Initializing Snowman consensus")
|
||||||
|
|
||||||
t.Config = config
|
t.Config = config
|
||||||
t.metrics.Initialize(config.Context.Log, config.Params.Namespace, config.Params.Metrics)
|
t.metrics.Initialize(
|
||||||
|
config.Context.Log,
|
||||||
|
config.Params.Namespace,
|
||||||
|
config.Params.Metrics,
|
||||||
|
)
|
||||||
|
|
||||||
t.onFinished = t.finishBootstrapping
|
t.onFinished = t.finishBootstrapping
|
||||||
t.bootstrapper.Initialize(config.BootstrapConfig)
|
t.bootstrapper.Initialize(config.BootstrapConfig)
|
||||||
|
@ -44,11 +58,19 @@ func (t *Transitive) Initialize(config Config) {
|
||||||
t.polls.m = make(map[uint32]poll)
|
t.polls.m = make(map[uint32]poll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when bootstrapping is finished, this will be called. This initializes the
|
||||||
|
// consensus engine with the last accepted block.
|
||||||
func (t *Transitive) finishBootstrapping() {
|
func (t *Transitive) finishBootstrapping() {
|
||||||
|
// set the bootstrapped mark to switch consensus modes
|
||||||
t.bootstrapped = true
|
t.bootstrapped = true
|
||||||
|
|
||||||
|
// initialize consensus to the last accepted blockID
|
||||||
tailID := t.Config.VM.LastAccepted()
|
tailID := t.Config.VM.LastAccepted()
|
||||||
t.Consensus.Initialize(t.Config.Context, t.Params, tailID)
|
t.Consensus.Initialize(t.Config.Context, t.Params, tailID)
|
||||||
|
|
||||||
|
// to maintain the invariant that oracle blocks are issued in the correct
|
||||||
|
// preferences, we need to handle the case that we are bootstrapping into an
|
||||||
|
// oracle block
|
||||||
tail, err := t.Config.VM.GetBlock(tailID)
|
tail, err := t.Config.VM.GetBlock(tailID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Config.Context.Log.Error("Failed to get last accepted block due to: %s", err)
|
t.Config.Context.Log.Error("Failed to get last accepted block due to: %s", err)
|
||||||
|
@ -58,9 +80,12 @@ func (t *Transitive) finishBootstrapping() {
|
||||||
switch blk := tail.(type) {
|
switch blk := tail.(type) {
|
||||||
case OracleBlock:
|
case OracleBlock:
|
||||||
for _, blk := range blk.Options() {
|
for _, blk := range blk.Options() {
|
||||||
|
// note that deliver will set the VM's preference
|
||||||
t.deliver(blk)
|
t.deliver(blk)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
// if there aren't blocks we need to deliver on startup, we need to set
|
||||||
|
// the preference to the last accepted block
|
||||||
t.Config.VM.SetPreference(tailID)
|
t.Config.VM.SetPreference(tailID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,15 +116,27 @@ func (t *Transitive) Context() *snow.Context { return t.Config.Context }
|
||||||
|
|
||||||
// Get implements the Engine interface
|
// Get implements the Engine interface
|
||||||
func (t *Transitive) Get(vdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
func (t *Transitive) Get(vdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
||||||
if blk, err := t.Config.VM.GetBlock(blkID); err == nil {
|
blk, err := t.Config.VM.GetBlock(blkID)
|
||||||
t.Config.Sender.Put(vdr, requestID, blkID, blk.Bytes())
|
if err != nil {
|
||||||
|
// If we failed to get the block, that means either an unexpected error
|
||||||
|
// has occurred, the validator is not following the protocol, or the
|
||||||
|
// block has been pruned.
|
||||||
|
t.Config.Context.Log.Warn("Get called for blockID %s errored with %s",
|
||||||
|
blkID,
|
||||||
|
err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Respond to the validator with the fetched block and the same requestID.
|
||||||
|
t.Config.Sender.Put(vdr, requestID, blkID, blk.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put implements the Engine interface
|
// Put implements the Engine interface
|
||||||
func (t *Transitive) Put(vdr ids.ShortID, requestID uint32, blkID ids.ID, blkBytes []byte) {
|
func (t *Transitive) Put(vdr ids.ShortID, requestID uint32, blkID ids.ID, blkBytes []byte) {
|
||||||
t.Config.Context.Log.Verbo("Put called for blockID %s", blkID)
|
t.Config.Context.Log.Verbo("Put called for blockID %s", blkID)
|
||||||
|
|
||||||
|
// if the engine hasn't been bootstrapped, forward the request to the
|
||||||
|
// bootstrapper
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.bootstrapper.Put(vdr, requestID, blkID, blkBytes)
|
t.bootstrapper.Put(vdr, requestID, blkID, blkBytes)
|
||||||
return
|
return
|
||||||
|
@ -110,32 +147,53 @@ func (t *Transitive) Put(vdr ids.ShortID, requestID uint32, blkID ids.ID, blkByt
|
||||||
t.Config.Context.Log.Debug("ParseBlock failed due to %s for block:\n%s",
|
t.Config.Context.Log.Debug("ParseBlock failed due to %s for block:\n%s",
|
||||||
err,
|
err,
|
||||||
formatting.DumpBytes{Bytes: blkBytes})
|
formatting.DumpBytes{Bytes: blkBytes})
|
||||||
t.GetFailed(vdr, requestID, blkID)
|
|
||||||
|
// because GetFailed doesn't utilize the assumption that we actually
|
||||||
|
// sent a Get message, we can safely call GetFailed here to potentially
|
||||||
|
// abandon the request.
|
||||||
|
t.GetFailed(vdr, requestID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insert the block into consensus. If the block has already been issued,
|
||||||
|
// this will be a noop. If this block has missing dependencies, vdr will
|
||||||
|
// receive requests to fill the ancestry. dependencies that have already
|
||||||
|
// been fetched, but with missing dependencies themselves won't be requested
|
||||||
|
// from the vdr.
|
||||||
t.insertFrom(vdr, blk)
|
t.insertFrom(vdr, blk)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFailed implements the Engine interface
|
// GetFailed implements the Engine interface
|
||||||
func (t *Transitive) GetFailed(vdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
func (t *Transitive) GetFailed(vdr ids.ShortID, requestID uint32) {
|
||||||
|
// if the engine hasn't been bootstrapped, forward the request to the
|
||||||
|
// bootstrapper
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.bootstrapper.GetFailed(vdr, requestID, blkID)
|
t.bootstrapper.GetFailed(vdr, requestID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.pending.Remove(blkID)
|
// we don't use the assumption that this function is called after a failed
|
||||||
t.blocked.Abandon(blkID)
|
// Get message. So we first check to see if we have an outsanding request
|
||||||
t.blkReqs.Remove(blkID)
|
// and also get what the request was for if it exists
|
||||||
|
blkID, ok := t.blkReqs.Remove(vdr, requestID)
|
||||||
|
if !ok {
|
||||||
|
t.Config.Context.Log.Warn("GetFailed called without sending the corresponding Get message from %s",
|
||||||
|
vdr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Tracks performance statistics
|
// because the get request was dropped, we no longer are expected blkID to
|
||||||
t.numBlockedBlk.Set(float64(t.pending.Len()))
|
// be issued.
|
||||||
|
t.blocked.Abandon(blkID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PullQuery implements the Engine interface
|
// PullQuery implements the Engine interface
|
||||||
func (t *Transitive) PullQuery(vdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
func (t *Transitive) PullQuery(vdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
||||||
|
// if the engine hasn't been bootstrapped, we aren't ready to respond to
|
||||||
|
// queries
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.Config.Context.Log.Debug("Dropping PullQuery for %s due to bootstrapping", blkID)
|
t.Config.Context.Log.Debug("Dropping PullQuery for %s due to bootstrapping",
|
||||||
|
blkID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,6 +204,8 @@ func (t *Transitive) PullQuery(vdr ids.ShortID, requestID uint32, blkID ids.ID)
|
||||||
requestID: requestID,
|
requestID: requestID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we aren't able to have issued this block, then it is a dependency for
|
||||||
|
// this reply
|
||||||
if !t.reinsertFrom(vdr, blkID) {
|
if !t.reinsertFrom(vdr, blkID) {
|
||||||
c.deps.Add(blkID)
|
c.deps.Add(blkID)
|
||||||
}
|
}
|
||||||
|
@ -154,18 +214,37 @@ func (t *Transitive) PullQuery(vdr ids.ShortID, requestID uint32, blkID ids.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushQuery implements the Engine interface
|
// PushQuery implements the Engine interface
|
||||||
func (t *Transitive) PushQuery(vdr ids.ShortID, requestID uint32, blkID ids.ID, blk []byte) {
|
func (t *Transitive) PushQuery(vdr ids.ShortID, requestID uint32, blkID ids.ID, blkBytes []byte) {
|
||||||
|
// if the engine hasn't been bootstrapped, we aren't ready to respond to
|
||||||
|
// queries
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.Config.Context.Log.Debug("Dropping PushQuery for %s due to bootstrapping", blkID)
|
t.Config.Context.Log.Debug("Dropping PushQuery for %s due to bootstrapping", blkID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Put(vdr, requestID, blkID, blk)
|
blk, err := t.Config.VM.ParseBlock(blkBytes)
|
||||||
t.PullQuery(vdr, requestID, blkID)
|
// If the parsing fails, we just drop the request, as we didn't ask for it
|
||||||
|
if err != nil {
|
||||||
|
t.Config.Context.Log.Warn("ParseBlock failed due to %s for block:\n%s",
|
||||||
|
err,
|
||||||
|
formatting.DumpBytes{Bytes: blkBytes})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the block into consensus. If the block has already been issued,
|
||||||
|
// this will be a noop. If this block has missing dependencies, vdr will
|
||||||
|
// receive requests to fill the ancestry. dependencies that have already
|
||||||
|
// been fetched, but with missing dependencies themselves won't be requested
|
||||||
|
// from the vdr.
|
||||||
|
t.insertFrom(vdr, blk)
|
||||||
|
|
||||||
|
// register the chit request
|
||||||
|
t.PullQuery(vdr, requestID, blk.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chits implements the Engine interface
|
// Chits implements the Engine interface
|
||||||
func (t *Transitive) Chits(vdr ids.ShortID, requestID uint32, votes ids.Set) {
|
func (t *Transitive) Chits(vdr ids.ShortID, requestID uint32, votes ids.Set) {
|
||||||
|
// if the engine hasn't been bootstrapped, we shouldn't be receiving chits
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.Config.Context.Log.Debug("Dropping Chits due to bootstrapping")
|
t.Config.Context.Log.Debug("Dropping Chits due to bootstrapping")
|
||||||
return
|
return
|
||||||
|
@ -173,7 +252,14 @@ func (t *Transitive) Chits(vdr ids.ShortID, requestID uint32, votes ids.Set) {
|
||||||
|
|
||||||
// Since this is snowman, there should only be one ID in the vote set
|
// Since this is snowman, there should only be one ID in the vote set
|
||||||
if votes.Len() != 1 {
|
if votes.Len() != 1 {
|
||||||
t.Config.Context.Log.Debug("Chits was called with the wrong number of votes %d. ValidatorID: %s, RequestID: %d", votes.Len(), vdr, requestID)
|
t.Config.Context.Log.Debug("Chits was called with the wrong number of votes %d. ValidatorID: %s, RequestID: %d",
|
||||||
|
votes.Len(),
|
||||||
|
vdr,
|
||||||
|
requestID)
|
||||||
|
|
||||||
|
// because QueryFailed doesn't utilize the assumption that we actually
|
||||||
|
// sent a Query message, we can safely call QueryFailed here to
|
||||||
|
// potentially abandon the request.
|
||||||
t.QueryFailed(vdr, requestID)
|
t.QueryFailed(vdr, requestID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -188,6 +274,8 @@ func (t *Transitive) Chits(vdr ids.ShortID, requestID uint32, votes ids.Set) {
|
||||||
response: vote,
|
response: vote,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we aren't able to have issued the vote's block, then it is a
|
||||||
|
// dependency for applying the vote
|
||||||
if !t.reinsertFrom(vdr, vote) {
|
if !t.reinsertFrom(vdr, vote) {
|
||||||
v.deps.Add(vote)
|
v.deps.Add(vote)
|
||||||
}
|
}
|
||||||
|
@ -197,6 +285,7 @@ func (t *Transitive) Chits(vdr ids.ShortID, requestID uint32, votes ids.Set) {
|
||||||
|
|
||||||
// QueryFailed implements the Engine interface
|
// QueryFailed implements the Engine interface
|
||||||
func (t *Transitive) QueryFailed(vdr ids.ShortID, requestID uint32) {
|
func (t *Transitive) QueryFailed(vdr ids.ShortID, requestID uint32) {
|
||||||
|
// if the engine hasn't been bootstrapped, we won't have sent a query
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.Config.Context.Log.Warn("Dropping QueryFailed due to bootstrapping")
|
t.Config.Context.Log.Warn("Dropping QueryFailed due to bootstrapping")
|
||||||
return
|
return
|
||||||
|
@ -211,6 +300,7 @@ func (t *Transitive) QueryFailed(vdr ids.ShortID, requestID uint32) {
|
||||||
|
|
||||||
// Notify implements the Engine interface
|
// Notify implements the Engine interface
|
||||||
func (t *Transitive) Notify(msg common.Message) {
|
func (t *Transitive) Notify(msg common.Message) {
|
||||||
|
// if the engine hasn't been bootstrapped, we shouldn't issuing blocks
|
||||||
if !t.bootstrapped {
|
if !t.bootstrapped {
|
||||||
t.Config.Context.Log.Warn("Dropping Notify due to bootstrapping")
|
t.Config.Context.Log.Warn("Dropping Notify due to bootstrapping")
|
||||||
return
|
return
|
||||||
|
@ -219,32 +309,53 @@ func (t *Transitive) Notify(msg common.Message) {
|
||||||
t.Config.Context.Log.Verbo("Snowman engine notified of %s from the vm", msg)
|
t.Config.Context.Log.Verbo("Snowman engine notified of %s from the vm", msg)
|
||||||
switch msg {
|
switch msg {
|
||||||
case common.PendingTxs:
|
case common.PendingTxs:
|
||||||
if blk, err := t.Config.VM.BuildBlock(); err == nil {
|
// the pending txs message means we should attempt to build a block.
|
||||||
|
blk, err := t.Config.VM.BuildBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Config.Context.Log.Verbo("VM.BuildBlock errored with %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// a newly created block is expected to be processing. If this check
|
||||||
|
// fails, there is potentially an error in the VM this engine is running
|
||||||
if status := blk.Status(); status != choices.Processing {
|
if status := blk.Status(); status != choices.Processing {
|
||||||
t.Config.Context.Log.Warn("Attempting to issue a block with status: %s, expected Processing", status)
|
t.Config.Context.Log.Warn("Attempting to issue a block with status: %s, expected Processing", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the newly created block should be built on top of the preferred
|
||||||
|
// block. Otherwise, the new block doesn't have the best chance of being
|
||||||
|
// confirmed.
|
||||||
parentID := blk.Parent().ID()
|
parentID := blk.Parent().ID()
|
||||||
if pref := t.Consensus.Preference(); !parentID.Equals(pref) {
|
if pref := t.Consensus.Preference(); !parentID.Equals(pref) {
|
||||||
t.Config.Context.Log.Warn("Built block with parent: %s, expected %s", parentID, pref)
|
t.Config.Context.Log.Warn("Built block with parent: %s, expected %s", parentID, pref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inserting the block shouldn't have any missing dependencies
|
||||||
if t.insertAll(blk) {
|
if t.insertAll(blk) {
|
||||||
t.Config.Context.Log.Verbo("Successfully issued new block from the VM")
|
t.Config.Context.Log.Verbo("Successfully issued new block from the VM")
|
||||||
} else {
|
} else {
|
||||||
t.Config.Context.Log.Warn("VM.BuildBlock returned a block that is pending for ancestors")
|
t.Config.Context.Log.Warn("VM.BuildBlock returned a block that is pending for ancestors")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
t.Config.Context.Log.Verbo("VM.BuildBlock errored with %s", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
t.Config.Context.Log.Warn("Unexpected message from the VM: %s", msg)
|
t.Config.Context.Log.Warn("Unexpected message from the VM: %s", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transitive) repoll() {
|
func (t *Transitive) repoll() {
|
||||||
|
// if we are issuing a repoll, we should gossip our current preferences to
|
||||||
|
// propagate the most likely branch as quickly as possible
|
||||||
prefID := t.Consensus.Preference()
|
prefID := t.Consensus.Preference()
|
||||||
|
|
||||||
|
for i := len(t.polls.m); i < t.Params.ConcurrentRepolls; i++ {
|
||||||
t.pullSample(prefID)
|
t.pullSample(prefID)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reinsertFrom attempts to issue the branch ending with a block, from only its
|
||||||
|
// ID, to consensus. Returns true if the block was added, or was previously
|
||||||
|
// added, to consensus. This is useful to check the local DB before requesting a
|
||||||
|
// block in case we have the block for some reason. If the block or a dependency
|
||||||
|
// is missing, the validator will be sent a Get message.
|
||||||
func (t *Transitive) reinsertFrom(vdr ids.ShortID, blkID ids.ID) bool {
|
func (t *Transitive) reinsertFrom(vdr ids.ShortID, blkID ids.ID) bool {
|
||||||
blk, err := t.Config.VM.GetBlock(blkID)
|
blk, err := t.Config.VM.GetBlock(blkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -254,44 +365,81 @@ func (t *Transitive) reinsertFrom(vdr ids.ShortID, blkID ids.ID) bool {
|
||||||
return t.insertFrom(vdr, blk)
|
return t.insertFrom(vdr, blk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insertFrom attempts to issue the branch ending with a block to consensus.
|
||||||
|
// Returns true if the block was added, or was previously added, to consensus.
|
||||||
|
// This is useful to check the local DB before requesting a block in case we
|
||||||
|
// have the block for some reason. If a dependency is missing, the validator
|
||||||
|
// will be sent a Get message.
|
||||||
func (t *Transitive) insertFrom(vdr ids.ShortID, blk snowman.Block) bool {
|
func (t *Transitive) insertFrom(vdr ids.ShortID, blk snowman.Block) bool {
|
||||||
blkID := blk.ID()
|
blkID := blk.ID()
|
||||||
|
// if the block has been issued, we don't need to insert it. if the block is
|
||||||
|
// already pending, we shouldn't attempt to insert it again yet
|
||||||
for !t.Consensus.Issued(blk) && !t.pending.Contains(blkID) {
|
for !t.Consensus.Issued(blk) && !t.pending.Contains(blkID) {
|
||||||
t.insert(blk)
|
t.insert(blk)
|
||||||
|
|
||||||
parent := blk.Parent()
|
blk = blk.Parent()
|
||||||
parentID := parent.ID()
|
blkID = blk.ID()
|
||||||
if parentStatus := parent.Status(); !parentStatus.Fetched() {
|
|
||||||
t.sendRequest(vdr, parentID)
|
// if the parent hasn't been fetched, we need to request it to issue the
|
||||||
|
// newly inserted block
|
||||||
|
if !blk.Status().Fetched() {
|
||||||
|
t.sendRequest(vdr, blkID)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
blk = parent
|
|
||||||
blkID = parentID
|
|
||||||
}
|
}
|
||||||
return !t.pending.Contains(blkID)
|
return t.Consensus.Issued(blk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insertAll attempts to issue the branch ending with a block to consensus.
|
||||||
|
// Returns true if the block was added, or was previously added, to consensus.
|
||||||
|
// This is useful to check the local DB before requesting a block in case we
|
||||||
|
// have the block for some reason. If a dependency is missing and the dependency
|
||||||
|
// hasn't been requested, the issuance will be abandoned.
|
||||||
func (t *Transitive) insertAll(blk snowman.Block) bool {
|
func (t *Transitive) insertAll(blk snowman.Block) bool {
|
||||||
blkID := blk.ID()
|
blkID := blk.ID()
|
||||||
for blk.Status().Fetched() && !t.Consensus.Issued(blk) && !t.pending.Contains(blkID) {
|
for blk.Status().Fetched() && !t.Consensus.Issued(blk) && !t.pending.Contains(blkID) {
|
||||||
t.insert(blk)
|
t.insert(blk)
|
||||||
|
|
||||||
blk = blk.Parent()
|
blk = blk.Parent()
|
||||||
}
|
blkID = blk.ID()
|
||||||
return !t.pending.Contains(blkID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if issuance the block was successful, this is the happy path
|
||||||
|
if t.Consensus.Issued(blk) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this branch is waiting on a block that we supposedly have a source of,
|
||||||
|
// we can just wait for that request to succeed or fail
|
||||||
|
if t.blkReqs.Contains(blkID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have no reason to expect that this block will be inserted, we
|
||||||
|
// should abandon the block to avoid a memory leak
|
||||||
|
t.blocked.Abandon(blkID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to insert the block to consensus. If the block's parent hasn't been
|
||||||
|
// issued, the insertion will block until the parent's issuance is abandoned or
|
||||||
|
// fulfilled
|
||||||
func (t *Transitive) insert(blk snowman.Block) {
|
func (t *Transitive) insert(blk snowman.Block) {
|
||||||
blkID := blk.ID()
|
blkID := blk.ID()
|
||||||
|
|
||||||
|
// mark that the block has been fetched but is pending
|
||||||
t.pending.Add(blkID)
|
t.pending.Add(blkID)
|
||||||
t.blkReqs.Remove(blkID)
|
|
||||||
|
// if we have any outstanding requests for this block, remove the pending
|
||||||
|
// requests
|
||||||
|
t.blkReqs.RemoveAny(blkID)
|
||||||
|
|
||||||
i := &issuer{
|
i := &issuer{
|
||||||
t: t,
|
t: t,
|
||||||
blk: blk,
|
blk: blk,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// block on the parent if needed
|
||||||
if parent := blk.Parent(); !t.Consensus.Issued(parent) {
|
if parent := blk.Parent(); !t.Consensus.Issued(parent) {
|
||||||
parentID := parent.ID()
|
parentID := parent.ID()
|
||||||
t.Config.Context.Log.Verbo("Block waiting for parent %s", parentID)
|
t.Config.Context.Log.Verbo("Block waiting for parent %s", parentID)
|
||||||
|
@ -306,17 +454,22 @@ func (t *Transitive) insert(blk snowman.Block) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transitive) sendRequest(vdr ids.ShortID, blkID ids.ID) {
|
func (t *Transitive) sendRequest(vdr ids.ShortID, blkID ids.ID) {
|
||||||
if !t.blkReqs.Contains(blkID) {
|
// only send one request at a time for a block
|
||||||
t.blkReqs.Add(blkID)
|
if t.blkReqs.Contains(blkID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
t.numBlkRequests.Set(float64(t.blkReqs.Len())) // Tracks performance statistics
|
t.Config.Context.Log.Verbo("Sending Get message for %s", blkID)
|
||||||
|
|
||||||
t.RequestID++
|
t.RequestID++
|
||||||
t.Config.Context.Log.Verbo("Sending Get message for %s", blkID)
|
t.blkReqs.Add(vdr, t.RequestID, blkID)
|
||||||
t.Config.Sender.Get(vdr, t.RequestID, blkID)
|
t.Config.Sender.Get(vdr, t.RequestID, blkID)
|
||||||
}
|
|
||||||
|
// Tracks performance statistics
|
||||||
|
t.numBlkRequests.Set(float64(t.blkReqs.Len()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send a pull request for this block ID
|
||||||
func (t *Transitive) pullSample(blkID ids.ID) {
|
func (t *Transitive) pullSample(blkID ids.ID) {
|
||||||
t.Config.Context.Log.Verbo("About to sample from: %s", t.Config.Validators)
|
t.Config.Context.Log.Verbo("About to sample from: %s", t.Config.Validators)
|
||||||
p := t.Consensus.Parameters()
|
p := t.Consensus.Parameters()
|
||||||
|
@ -326,15 +479,22 @@ func (t *Transitive) pullSample(blkID ids.ID) {
|
||||||
vdrSet.Add(vdr.ID())
|
vdrSet.Add(vdr.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
t.RequestID++
|
if numVdrs := len(vdrs); numVdrs != p.K {
|
||||||
if numVdrs := len(vdrs); numVdrs == p.K && t.polls.Add(t.RequestID, vdrSet.Len()) {
|
|
||||||
t.Config.Sender.PullQuery(vdrSet, t.RequestID, blkID)
|
|
||||||
} else if numVdrs < p.K {
|
|
||||||
t.Config.Context.Log.Error("Query for %s was dropped due to an insufficient number of validators", blkID)
|
t.Config.Context.Log.Error("Query for %s was dropped due to an insufficient number of validators", blkID)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transitive) pushSample(blk snowman.Block) bool {
|
t.RequestID++
|
||||||
|
if !t.polls.Add(t.RequestID, vdrSet.Len()) {
|
||||||
|
t.Config.Context.Log.Error("Query for %s was dropped due to use of a duplicated requestID", blkID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Config.Sender.PullQuery(vdrSet, t.RequestID, blkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a push request for this block
|
||||||
|
func (t *Transitive) pushSample(blk snowman.Block) {
|
||||||
t.Config.Context.Log.Verbo("About to sample from: %s", t.Config.Validators)
|
t.Config.Context.Log.Verbo("About to sample from: %s", t.Config.Validators)
|
||||||
p := t.Consensus.Parameters()
|
p := t.Consensus.Parameters()
|
||||||
vdrs := t.Config.Validators.Sample(p.K)
|
vdrs := t.Config.Validators.Sample(p.K)
|
||||||
|
@ -343,15 +503,20 @@ func (t *Transitive) pushSample(blk snowman.Block) bool {
|
||||||
vdrSet.Add(vdr.ID())
|
vdrSet.Add(vdr.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
t.RequestID++
|
blkID := blk.ID()
|
||||||
queryIssued := false
|
if numVdrs := len(vdrs); numVdrs != p.K {
|
||||||
if numVdrs := len(vdrs); numVdrs == p.K && t.polls.Add(t.RequestID, vdrSet.Len()) {
|
t.Config.Context.Log.Error("Query for %s was dropped due to an insufficient number of validators", blkID)
|
||||||
t.Config.Sender.PushQuery(vdrSet, t.RequestID, blk.ID(), blk.Bytes())
|
return
|
||||||
queryIssued = true
|
|
||||||
} else if numVdrs < p.K {
|
|
||||||
t.Config.Context.Log.Error("Query for %s was dropped due to an insufficient number of validators", blk.ID())
|
|
||||||
}
|
}
|
||||||
return queryIssued
|
|
||||||
|
t.RequestID++
|
||||||
|
if !t.polls.Add(t.RequestID, vdrSet.Len()) {
|
||||||
|
t.Config.Context.Log.Error("Query for %s was dropped due to use of a duplicated requestID", blkID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Config.Sender.PushQuery(vdrSet, t.RequestID, blkID, blk.Bytes())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transitive) deliver(blk snowman.Block) {
|
func (t *Transitive) deliver(blk snowman.Block) {
|
||||||
|
@ -359,11 +524,14 @@ func (t *Transitive) deliver(blk snowman.Block) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we are adding the block to consensus, so it is no longer pending
|
||||||
blkID := blk.ID()
|
blkID := blk.ID()
|
||||||
t.pending.Remove(blkID)
|
t.pending.Remove(blkID)
|
||||||
|
|
||||||
if err := blk.Verify(); err != nil {
|
if err := blk.Verify(); err != nil {
|
||||||
t.Config.Context.Log.Debug("Block failed verification due to %s, dropping block", err)
|
t.Config.Context.Log.Debug("Block failed verification due to %s, dropping block", err)
|
||||||
|
|
||||||
|
// if verify fails, then all decedents are also invalid
|
||||||
t.blocked.Abandon(blkID)
|
t.blocked.Abandon(blkID)
|
||||||
t.numBlockedBlk.Set(float64(t.pending.Len())) // Tracks performance statistics
|
t.numBlockedBlk.Set(float64(t.pending.Len())) // Tracks performance statistics
|
||||||
return
|
return
|
||||||
|
@ -371,8 +539,10 @@ func (t *Transitive) deliver(blk snowman.Block) {
|
||||||
|
|
||||||
t.Config.Context.Log.Verbo("Adding block to consensus: %s", blkID)
|
t.Config.Context.Log.Verbo("Adding block to consensus: %s", blkID)
|
||||||
t.Consensus.Add(blk)
|
t.Consensus.Add(blk)
|
||||||
polled := t.pushSample(blk)
|
|
||||||
|
|
||||||
|
// Add all the oracle blocks if they exist. We call verify on all the blocks
|
||||||
|
// and add them to consensus before marking anything as fulfilled to avoid
|
||||||
|
// any potential reentrant bugs.
|
||||||
added := []snowman.Block{}
|
added := []snowman.Block{}
|
||||||
dropped := []snowman.Block{}
|
dropped := []snowman.Block{}
|
||||||
switch blk := blk.(type) {
|
switch blk := blk.(type) {
|
||||||
|
@ -380,20 +550,23 @@ func (t *Transitive) deliver(blk snowman.Block) {
|
||||||
for _, blk := range blk.Options() {
|
for _, blk := range blk.Options() {
|
||||||
if err := blk.Verify(); err != nil {
|
if err := blk.Verify(); err != nil {
|
||||||
t.Config.Context.Log.Debug("Block failed verification due to %s, dropping block", err)
|
t.Config.Context.Log.Debug("Block failed verification due to %s, dropping block", err)
|
||||||
t.blocked.Abandon(blk.ID())
|
|
||||||
dropped = append(dropped, blk)
|
dropped = append(dropped, blk)
|
||||||
} else {
|
} else {
|
||||||
t.Consensus.Add(blk)
|
t.Consensus.Add(blk)
|
||||||
t.pushSample(blk)
|
|
||||||
added = append(added, blk)
|
added = append(added, blk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Config.VM.SetPreference(t.Consensus.Preference())
|
t.Config.VM.SetPreference(t.Consensus.Preference())
|
||||||
t.blocked.Fulfill(blkID)
|
|
||||||
|
|
||||||
|
// launch a query for the newly added block
|
||||||
|
t.pushSample(blk)
|
||||||
|
|
||||||
|
t.blocked.Fulfill(blkID)
|
||||||
for _, blk := range added {
|
for _, blk := range added {
|
||||||
|
t.pushSample(blk)
|
||||||
|
|
||||||
blkID := blk.ID()
|
blkID := blk.ID()
|
||||||
t.pending.Remove(blkID)
|
t.pending.Remove(blkID)
|
||||||
t.blocked.Fulfill(blkID)
|
t.blocked.Fulfill(blkID)
|
||||||
|
@ -404,9 +577,8 @@ func (t *Transitive) deliver(blk snowman.Block) {
|
||||||
t.blocked.Abandon(blkID)
|
t.blocked.Abandon(blkID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if polled && len(t.polls.m) < t.Params.ConcurrentRepolls {
|
// If we should issue multiple queries at the same time, we need to repoll
|
||||||
t.repoll()
|
t.repoll()
|
||||||
}
|
|
||||||
|
|
||||||
// Tracks performance statistics
|
// Tracks performance statistics
|
||||||
t.numBlkRequests.Set(float64(t.blkReqs.Len()))
|
t.numBlkRequests.Set(float64(t.blkReqs.Len()))
|
||||||
|
|
|
@ -102,7 +102,9 @@ func TestEngineAdd(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
asked := new(bool)
|
asked := new(bool)
|
||||||
sender.GetF = func(inVdr ids.ShortID, _ uint32, blkID ids.ID) {
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(inVdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
if *asked {
|
if *asked {
|
||||||
t.Fatalf("Asked multiple times")
|
t.Fatalf("Asked multiple times")
|
||||||
}
|
}
|
||||||
|
@ -136,7 +138,7 @@ func TestEngineAdd(t *testing.T) {
|
||||||
|
|
||||||
vm.ParseBlockF = func(b []byte) (snowman.Block, error) { return nil, errParseBlock }
|
vm.ParseBlockF = func(b []byte) (snowman.Block, error) { return nil, errParseBlock }
|
||||||
|
|
||||||
te.Put(vdr.ID(), 0, blk.Parent().ID(), nil)
|
te.Put(vdr.ID(), *reqID, blk.Parent().ID(), nil)
|
||||||
|
|
||||||
vm.ParseBlockF = nil
|
vm.ParseBlockF = nil
|
||||||
|
|
||||||
|
@ -906,7 +908,11 @@ func TestEngineAbandonQuery(t *testing.T) {
|
||||||
panic("Should have failed")
|
panic("Should have failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sender.CantGet = false
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(_ ids.ShortID, requestID uint32, _ ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
}
|
||||||
|
|
||||||
te.PullQuery(vdr.ID(), 0, blkID)
|
te.PullQuery(vdr.ID(), 0, blkID)
|
||||||
|
|
||||||
|
@ -914,7 +920,7 @@ func TestEngineAbandonQuery(t *testing.T) {
|
||||||
t.Fatalf("Should have blocked on request")
|
t.Fatalf("Should have blocked on request")
|
||||||
}
|
}
|
||||||
|
|
||||||
te.GetFailed(vdr.ID(), 0, blkID)
|
te.GetFailed(vdr.ID(), *reqID)
|
||||||
|
|
||||||
if len(te.blocked) != 0 {
|
if len(te.blocked) != 0 {
|
||||||
t.Fatalf("Should have removed request")
|
t.Fatalf("Should have removed request")
|
||||||
|
@ -947,7 +953,12 @@ func TestEngineAbandonChit(t *testing.T) {
|
||||||
panic("Should have failed")
|
panic("Should have failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sender.CantGet = false
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(_ ids.ShortID, requestID uint32, _ ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
}
|
||||||
|
|
||||||
fakeBlkIDSet := ids.Set{}
|
fakeBlkIDSet := ids.Set{}
|
||||||
fakeBlkIDSet.Add(fakeBlkID)
|
fakeBlkIDSet.Add(fakeBlkID)
|
||||||
te.Chits(vdr.ID(), 0, fakeBlkIDSet)
|
te.Chits(vdr.ID(), 0, fakeBlkIDSet)
|
||||||
|
@ -956,7 +967,7 @@ func TestEngineAbandonChit(t *testing.T) {
|
||||||
t.Fatalf("Should have blocked on request")
|
t.Fatalf("Should have blocked on request")
|
||||||
}
|
}
|
||||||
|
|
||||||
te.GetFailed(vdr.ID(), 0, fakeBlkID)
|
te.GetFailed(vdr.ID(), *reqID)
|
||||||
|
|
||||||
if len(te.blocked) != 0 {
|
if len(te.blocked) != 0 {
|
||||||
t.Fatalf("Should have removed request")
|
t.Fatalf("Should have removed request")
|
||||||
|
@ -1105,14 +1116,18 @@ func TestEngineRetryFetch(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.CantGetBlock = false
|
vm.CantGetBlock = false
|
||||||
sender.CantGet = false
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(_ ids.ShortID, requestID uint32, _ ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
}
|
||||||
|
|
||||||
te.PullQuery(vdr.ID(), 0, missingBlk.ID())
|
te.PullQuery(vdr.ID(), 0, missingBlk.ID())
|
||||||
|
|
||||||
vm.CantGetBlock = true
|
vm.CantGetBlock = true
|
||||||
sender.CantGet = true
|
sender.GetF = nil
|
||||||
|
|
||||||
te.GetFailed(vdr.ID(), 0, missingBlk.ID())
|
te.GetFailed(vdr.ID(), *reqID)
|
||||||
|
|
||||||
vm.CantGetBlock = false
|
vm.CantGetBlock = false
|
||||||
|
|
||||||
|
@ -1124,7 +1139,7 @@ func TestEngineRetryFetch(t *testing.T) {
|
||||||
te.PullQuery(vdr.ID(), 0, missingBlk.ID())
|
te.PullQuery(vdr.ID(), 0, missingBlk.ID())
|
||||||
|
|
||||||
vm.CantGetBlock = true
|
vm.CantGetBlock = true
|
||||||
sender.CantGet = true
|
sender.GetF = nil
|
||||||
|
|
||||||
if !*called {
|
if !*called {
|
||||||
t.Fatalf("Should have requested the block again")
|
t.Fatalf("Should have requested the block again")
|
||||||
|
@ -1220,3 +1235,290 @@ func TestEngineGossip(t *testing.T) {
|
||||||
t.Fatalf("Should have gossiped the block")
|
t.Fatalf("Should have gossiped the block")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEngineInvalidBlockIgnoredFromUnexpectedPeer(t *testing.T) {
|
||||||
|
vdr, vdrs, sender, vm, te, gBlk := setup(t)
|
||||||
|
|
||||||
|
secondVdr := validators.GenerateRandomValidator(1)
|
||||||
|
vdrs.Add(secondVdr)
|
||||||
|
|
||||||
|
sender.Default(true)
|
||||||
|
|
||||||
|
missingBlk := &Blk{
|
||||||
|
parent: gBlk,
|
||||||
|
id: GenerateID(),
|
||||||
|
height: 1,
|
||||||
|
status: choices.Unknown,
|
||||||
|
bytes: []byte{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingBlk := &Blk{
|
||||||
|
parent: missingBlk,
|
||||||
|
id: GenerateID(),
|
||||||
|
height: 2,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: []byte{2},
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := new(bool)
|
||||||
|
vm.ParseBlockF = func(b []byte) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, pendingBlk.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return pendingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.GetBlockF = func(blkID ids.ID) (snowman.Block, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case blkID.Equals(pendingBlk.ID()):
|
||||||
|
return pendingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(reqVdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
if !reqVdr.Equals(vdr.ID()) {
|
||||||
|
t.Fatalf("Wrong validator requested")
|
||||||
|
}
|
||||||
|
if !blkID.Equals(missingBlk.ID()) {
|
||||||
|
t.Fatalf("Wrong block requested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
te.PushQuery(vdr.ID(), 0, pendingBlk.ID(), pendingBlk.Bytes())
|
||||||
|
|
||||||
|
te.Put(secondVdr.ID(), *reqID, missingBlk.ID(), []byte{3})
|
||||||
|
|
||||||
|
*parsed = false
|
||||||
|
vm.ParseBlockF = func(b []byte) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, missingBlk.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return missingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
vm.GetBlockF = func(blkID ids.ID) (snowman.Block, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case blkID.Equals(missingBlk.ID()):
|
||||||
|
return missingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
sender.CantPushQuery = false
|
||||||
|
sender.CantChits = false
|
||||||
|
|
||||||
|
missingBlk.status = choices.Processing
|
||||||
|
|
||||||
|
te.Put(vdr.ID(), *reqID, missingBlk.ID(), missingBlk.Bytes())
|
||||||
|
|
||||||
|
pref := te.Consensus.Preference()
|
||||||
|
if !pref.Equals(pendingBlk.ID()) {
|
||||||
|
t.Fatalf("Shouldn't have abandoned the pending block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnginePushQueryRequestIDConflict(t *testing.T) {
|
||||||
|
vdr, _, sender, vm, te, gBlk := setup(t)
|
||||||
|
|
||||||
|
sender.Default(true)
|
||||||
|
|
||||||
|
missingBlk := &Blk{
|
||||||
|
parent: gBlk,
|
||||||
|
id: GenerateID(),
|
||||||
|
height: 1,
|
||||||
|
status: choices.Unknown,
|
||||||
|
bytes: []byte{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingBlk := &Blk{
|
||||||
|
parent: missingBlk,
|
||||||
|
id: GenerateID(),
|
||||||
|
height: 2,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: []byte{2},
|
||||||
|
}
|
||||||
|
|
||||||
|
randomBlkID := GenerateID()
|
||||||
|
|
||||||
|
parsed := new(bool)
|
||||||
|
vm.ParseBlockF = func(b []byte) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, pendingBlk.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return pendingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.GetBlockF = func(blkID ids.ID) (snowman.Block, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case blkID.Equals(pendingBlk.ID()):
|
||||||
|
return pendingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
reqID := new(uint32)
|
||||||
|
sender.GetF = func(reqVdr ids.ShortID, requestID uint32, blkID ids.ID) {
|
||||||
|
*reqID = requestID
|
||||||
|
if !reqVdr.Equals(vdr.ID()) {
|
||||||
|
t.Fatalf("Wrong validator requested")
|
||||||
|
}
|
||||||
|
if !blkID.Equals(missingBlk.ID()) {
|
||||||
|
t.Fatalf("Wrong block requested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
te.PushQuery(vdr.ID(), 0, pendingBlk.ID(), pendingBlk.Bytes())
|
||||||
|
|
||||||
|
sender.GetF = nil
|
||||||
|
sender.CantGet = false
|
||||||
|
|
||||||
|
te.PushQuery(vdr.ID(), *reqID, randomBlkID, []byte{3})
|
||||||
|
|
||||||
|
*parsed = false
|
||||||
|
vm.ParseBlockF = func(b []byte) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, missingBlk.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return missingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
vm.GetBlockF = func(blkID ids.ID) (snowman.Block, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case blkID.Equals(missingBlk.ID()):
|
||||||
|
return missingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
sender.CantPushQuery = false
|
||||||
|
sender.CantChits = false
|
||||||
|
|
||||||
|
te.Put(vdr.ID(), *reqID, missingBlk.ID(), missingBlk.Bytes())
|
||||||
|
|
||||||
|
pref := te.Consensus.Preference()
|
||||||
|
if !pref.Equals(pendingBlk.ID()) {
|
||||||
|
t.Fatalf("Shouldn't have abandoned the pending block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngineAggressivePolling(t *testing.T) {
|
||||||
|
config := DefaultConfig()
|
||||||
|
|
||||||
|
config.Params.ConcurrentRepolls = 2
|
||||||
|
|
||||||
|
vdr := validators.GenerateRandomValidator(1)
|
||||||
|
|
||||||
|
vals := validators.NewSet()
|
||||||
|
config.Validators = vals
|
||||||
|
|
||||||
|
vals.Add(vdr)
|
||||||
|
|
||||||
|
sender := &common.SenderTest{}
|
||||||
|
sender.T = t
|
||||||
|
config.Sender = sender
|
||||||
|
|
||||||
|
sender.Default(true)
|
||||||
|
|
||||||
|
vm := &VMTest{}
|
||||||
|
vm.T = t
|
||||||
|
config.VM = vm
|
||||||
|
|
||||||
|
vm.Default(true)
|
||||||
|
vm.CantSetPreference = false
|
||||||
|
|
||||||
|
gBlk := &Blk{
|
||||||
|
id: GenerateID(),
|
||||||
|
status: choices.Accepted,
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.LastAcceptedF = func() ids.ID { return gBlk.ID() }
|
||||||
|
sender.CantGetAcceptedFrontier = false
|
||||||
|
|
||||||
|
te := &Transitive{}
|
||||||
|
|
||||||
|
te.Initialize(config)
|
||||||
|
|
||||||
|
vm.GetBlockF = func(blkID ids.ID) (snowman.Block, error) {
|
||||||
|
if !blkID.Equals(gBlk.ID()) {
|
||||||
|
t.Fatalf("Wrong block requested")
|
||||||
|
}
|
||||||
|
return gBlk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
te.finishBootstrapping()
|
||||||
|
|
||||||
|
vm.GetBlockF = nil
|
||||||
|
vm.LastAcceptedF = nil
|
||||||
|
sender.CantGetAcceptedFrontier = true
|
||||||
|
|
||||||
|
sender.Default(true)
|
||||||
|
|
||||||
|
pendingBlk := &Blk{
|
||||||
|
parent: gBlk,
|
||||||
|
id: GenerateID(),
|
||||||
|
height: 2,
|
||||||
|
status: choices.Processing,
|
||||||
|
bytes: []byte{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := new(bool)
|
||||||
|
vm.ParseBlockF = func(b []byte) (snowman.Block, error) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(b, pendingBlk.Bytes()):
|
||||||
|
*parsed = true
|
||||||
|
return pendingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.GetBlockF = func(blkID ids.ID) (snowman.Block, error) {
|
||||||
|
if !*parsed {
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case blkID.Equals(pendingBlk.ID()):
|
||||||
|
return pendingBlk, nil
|
||||||
|
}
|
||||||
|
return nil, errUnknownBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
numPushed := new(int)
|
||||||
|
sender.PushQueryF = func(_ ids.ShortSet, _ uint32, _ ids.ID, _ []byte) { *numPushed++ }
|
||||||
|
|
||||||
|
numPulled := new(int)
|
||||||
|
sender.PullQueryF = func(_ ids.ShortSet, _ uint32, _ ids.ID) { *numPulled++ }
|
||||||
|
|
||||||
|
te.Put(vdr.ID(), 0, pendingBlk.ID(), pendingBlk.Bytes())
|
||||||
|
|
||||||
|
if *numPushed != 1 {
|
||||||
|
t.Fatalf("Should have initially sent a push query")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *numPulled != 1 {
|
||||||
|
t.Fatalf("Should have sent an additional pull query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -56,11 +56,8 @@ func (v *voter) Update() {
|
||||||
}
|
}
|
||||||
|
|
||||||
v.t.Config.Context.Log.Verbo("Snowman engine can't quiesce")
|
v.t.Config.Context.Log.Verbo("Snowman engine can't quiesce")
|
||||||
|
|
||||||
if len(v.t.polls.m) < v.t.Config.Params.ConcurrentRepolls {
|
|
||||||
v.t.repoll()
|
v.t.repoll()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (v *voter) bubbleVotes(votes ids.Bag) ids.Bag {
|
func (v *voter) bubbleVotes(votes ids.Bag) ids.Bag {
|
||||||
bubbledVotes := ids.Bag{}
|
bubbledVotes := ids.Bag{}
|
||||||
|
|
|
@ -78,7 +78,7 @@ func (h *Handler) dispatchMsg(msg message) bool {
|
||||||
case getMsg:
|
case getMsg:
|
||||||
h.engine.Get(msg.validatorID, msg.requestID, msg.containerID)
|
h.engine.Get(msg.validatorID, msg.requestID, msg.containerID)
|
||||||
case getFailedMsg:
|
case getFailedMsg:
|
||||||
h.engine.GetFailed(msg.validatorID, msg.requestID, msg.containerID)
|
h.engine.GetFailed(msg.validatorID, msg.requestID)
|
||||||
case putMsg:
|
case putMsg:
|
||||||
h.engine.Put(msg.validatorID, msg.requestID, msg.containerID, msg.container)
|
h.engine.Put(msg.validatorID, msg.requestID, msg.containerID, msg.container)
|
||||||
case pushQueryMsg:
|
case pushQueryMsg:
|
||||||
|
@ -185,12 +185,11 @@ func (h *Handler) Put(validatorID ids.ShortID, requestID uint32, containerID ids
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFailed passes a GetFailed message to the consensus engine.
|
// GetFailed passes a GetFailed message to the consensus engine.
|
||||||
func (h *Handler) GetFailed(validatorID ids.ShortID, requestID uint32, containerID ids.ID) {
|
func (h *Handler) GetFailed(validatorID ids.ShortID, requestID uint32) {
|
||||||
h.msgs <- message{
|
h.msgs <- message{
|
||||||
messageType: getFailedMsg,
|
messageType: getFailedMsg,
|
||||||
validatorID: validatorID,
|
validatorID: validatorID,
|
||||||
requestID: requestID,
|
requestID: requestID,
|
||||||
containerID: containerID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,6 @@ type ExternalRouter interface {
|
||||||
type InternalRouter interface {
|
type InternalRouter interface {
|
||||||
GetAcceptedFrontierFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32)
|
GetAcceptedFrontierFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32)
|
||||||
GetAcceptedFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32)
|
GetAcceptedFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32)
|
||||||
GetFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32, containerID ids.ID)
|
GetFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32)
|
||||||
QueryFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32)
|
QueryFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32)
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,13 +187,13 @@ func (sr *ChainRouter) Put(validatorID ids.ShortID, chainID ids.ID, requestID ui
|
||||||
|
|
||||||
// GetFailed routes an incoming GetFailed message from the validator with ID [validatorID]
|
// GetFailed routes an incoming GetFailed message from the validator with ID [validatorID]
|
||||||
// to the consensus engine working on the chain with ID [chainID]
|
// to the consensus engine working on the chain with ID [chainID]
|
||||||
func (sr *ChainRouter) GetFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32, containerID ids.ID) {
|
func (sr *ChainRouter) GetFailed(validatorID ids.ShortID, chainID ids.ID, requestID uint32) {
|
||||||
sr.lock.RLock()
|
sr.lock.RLock()
|
||||||
defer sr.lock.RUnlock()
|
defer sr.lock.RUnlock()
|
||||||
|
|
||||||
sr.timeouts.Cancel(validatorID, chainID, requestID)
|
sr.timeouts.Cancel(validatorID, chainID, requestID)
|
||||||
if chain, exists := sr.chains[chainID.Key()]; exists {
|
if chain, exists := sr.chains[chainID.Key()]; exists {
|
||||||
chain.GetFailed(validatorID, requestID, containerID)
|
chain.GetFailed(validatorID, requestID)
|
||||||
} else {
|
} else {
|
||||||
sr.log.Debug("message referenced a chain, %s, this node doesn't validate", chainID)
|
sr.log.Debug("message referenced a chain, %s, this node doesn't validate", chainID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ func (s *Sender) Get(validatorID ids.ShortID, requestID uint32, containerID ids.
|
||||||
// Add a timeout -- if we don't get a response before the timeout expires,
|
// Add a timeout -- if we don't get a response before the timeout expires,
|
||||||
// send this consensus engine a GetFailed message
|
// send this consensus engine a GetFailed message
|
||||||
s.timeouts.Register(validatorID, s.ctx.ChainID, requestID, func() {
|
s.timeouts.Register(validatorID, s.ctx.ChainID, requestID, func() {
|
||||||
s.router.GetFailed(validatorID, s.ctx.ChainID, requestID, containerID)
|
s.router.GetFailed(validatorID, s.ctx.ChainID, requestID)
|
||||||
})
|
})
|
||||||
s.sender.Get(validatorID, s.ctx.ChainID, requestID, containerID)
|
s.sender.Get(validatorID, s.ctx.ChainID, requestID, containerID)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue