diff --git a/.gitignore b/.gitignore
index ebd0ef239b..9781154449 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,7 @@ crypto/libs
*~
*.swp
*.swo
+*.swn
# Mac
.DS_Store
diff --git a/Makefile b/Makefile
index a418c7e35e..b9ad2e319b 100644
--- a/Makefile
+++ b/Makefile
@@ -85,7 +85,7 @@ GOLDFLAGS := $(GOLDFLAGS_BASE) \
UNIT_TEST_SOURCES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && go list ./... | grep -v /go-algorand/test/ ))
ALGOD_API_PACKAGES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && cd daemon/algod/api; go list ./... ))
-MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./node ./ledger ./ledger/ledgercore ./ledger/store ./ledger/encoded ./stateproof ./data/account ./daemon/algod/api/spec/v2
+MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./network ./node ./ledger ./ledger/ledgercore ./ledger/store ./ledger/encoded ./stateproof ./data/account ./daemon/algod/api/spec/v2
default: build
diff --git a/config/localTemplate.go b/config/localTemplate.go
index 020361918d..cdf284c320 100644
--- a/config/localTemplate.go
+++ b/config/localTemplate.go
@@ -56,7 +56,11 @@ type Local struct {
// 1 * time.Minute = 60000000000 ns
ReconnectTime time.Duration `version[0]:"60" version[1]:"60000000000"`
- // what we should tell people to connect to
+ // The public address to connect to that is advertised to other nodes.
+ // For MainNet relays, make sure this entry includes the full SRV host name
+ // plus the publicly-accessible port number.
+ // A valid entry will avoid "self-gossip" and is used for identity exchange
+ // to deduplicate redundant connections
PublicAddress string `version[0]:""`
MaxConnectionsPerIP int `version[3]:"30" version[27]:"15"`
diff --git a/network/connPerfMon.go b/network/connPerfMon.go
index e74614ae10..d254aa1279 100644
--- a/network/connPerfMon.go
+++ b/network/connPerfMon.go
@@ -25,6 +25,7 @@ import (
"github.com/algorand/go-algorand/crypto"
)
+//msgp:ignore pmStage
type pmStage int
const (
diff --git a/network/msgp_gen.go b/network/msgp_gen.go
new file mode 100644
index 0000000000..70b03ccfc7
--- /dev/null
+++ b/network/msgp_gen.go
@@ -0,0 +1,1088 @@
+package network
+
+// Code generated by github.com/algorand/msgp DO NOT EDIT.
+
+import (
+ "github.com/algorand/msgp/msgp"
+)
+
+// The following msgp objects are implemented in this file:
+// disconnectReason
+// |-----> MarshalMsg
+// |-----> CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> Msgsize
+// |-----> MsgIsZero
+//
+// identityChallenge
+// |-----> (*) MarshalMsg
+// |-----> (*) CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> (*) Msgsize
+// |-----> (*) MsgIsZero
+//
+// identityChallengeResponse
+// |-----> (*) MarshalMsg
+// |-----> (*) CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> (*) Msgsize
+// |-----> (*) MsgIsZero
+//
+// identityChallengeResponseSigned
+// |-----> (*) MarshalMsg
+// |-----> (*) CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> (*) Msgsize
+// |-----> (*) MsgIsZero
+//
+// identityChallengeSigned
+// |-----> (*) MarshalMsg
+// |-----> (*) CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> (*) Msgsize
+// |-----> (*) MsgIsZero
+//
+// identityChallengeValue
+// |-----> (*) MarshalMsg
+// |-----> (*) CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> (*) Msgsize
+// |-----> (*) MsgIsZero
+//
+// identityVerificationMessage
+// |-----> (*) MarshalMsg
+// |-----> (*) CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> (*) Msgsize
+// |-----> (*) MsgIsZero
+//
+// identityVerificationMessageSigned
+// |-----> (*) MarshalMsg
+// |-----> (*) CanMarshalMsg
+// |-----> (*) UnmarshalMsg
+// |-----> (*) CanUnmarshalMsg
+// |-----> (*) Msgsize
+// |-----> (*) MsgIsZero
+//
+
+// MarshalMsg implements msgp.Marshaler
+func (z disconnectReason) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendString(o, string(z))
+ return
+}
+
+func (_ disconnectReason) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(disconnectReason)
+ if !ok {
+ _, ok = (z).(*disconnectReason)
+ }
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *disconnectReason) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 string
+ zb0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = disconnectReason(zb0001)
+ }
+ o = bts
+ return
+}
+
+func (_ *disconnectReason) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*disconnectReason)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z disconnectReason) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z disconnectReason) MsgIsZero() bool {
+ return z == ""
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *identityChallenge) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ // omitempty: check for empty values
+ zb0002Len := uint32(3)
+ var zb0002Mask uint8 /* 4 bits */
+ if len((*z).PublicAddress) == 0 {
+ zb0002Len--
+ zb0002Mask |= 0x2
+ }
+ if (*z).Challenge == (identityChallengeValue{}) {
+ zb0002Len--
+ zb0002Mask |= 0x4
+ }
+ if (*z).Key.MsgIsZero() {
+ zb0002Len--
+ zb0002Mask |= 0x8
+ }
+ // variable map header, size zb0002Len
+ o = append(o, 0x80|uint8(zb0002Len))
+ if zb0002Len != 0 {
+ if (zb0002Mask & 0x2) == 0 { // if not empty
+ // string "a"
+ o = append(o, 0xa1, 0x61)
+ o = msgp.AppendBytes(o, (*z).PublicAddress)
+ }
+ if (zb0002Mask & 0x4) == 0 { // if not empty
+ // string "c"
+ o = append(o, 0xa1, 0x63)
+ o = msgp.AppendBytes(o, ((*z).Challenge)[:])
+ }
+ if (zb0002Mask & 0x8) == 0 { // if not empty
+ // string "pk"
+ o = append(o, 0xa2, 0x70, 0x6b)
+ o = (*z).Key.MarshalMsg(o)
+ }
+ }
+ return
+}
+
+func (_ *identityChallenge) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallenge)
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *identityChallenge) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0002 int
+ var zb0003 bool
+ zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0002 > 0 {
+ zb0002--
+ bts, err = (*z).Key.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Key")
+ return
+ }
+ }
+ if zb0002 > 0 {
+ zb0002--
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Challenge")
+ return
+ }
+ }
+ if zb0002 > 0 {
+ zb0002--
+ var zb0004 int
+ zb0004, err = msgp.ReadBytesBytesHeader(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "PublicAddress")
+ return
+ }
+ if zb0004 > maxAddressLen {
+ err = msgp.ErrOverflow(uint64(zb0004), uint64(maxAddressLen))
+ return
+ }
+ (*z).PublicAddress, bts, err = msgp.ReadBytesBytes(bts, (*z).PublicAddress)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "PublicAddress")
+ return
+ }
+ }
+ if zb0002 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0002)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0003 {
+ (*z) = identityChallenge{}
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch string(field) {
+ case "pk":
+ bts, err = (*z).Key.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ case "c":
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "Challenge")
+ return
+ }
+ case "a":
+ var zb0005 int
+ zb0005, err = msgp.ReadBytesBytesHeader(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PublicAddress")
+ return
+ }
+ if zb0005 > maxAddressLen {
+ err = msgp.ErrOverflow(uint64(zb0005), uint64(maxAddressLen))
+ return
+ }
+ (*z).PublicAddress, bts, err = msgp.ReadBytesBytes(bts, (*z).PublicAddress)
+ if err != nil {
+ err = msgp.WrapError(err, "PublicAddress")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+func (_ *identityChallenge) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallenge)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *identityChallenge) Msgsize() (s int) {
+ s = 1 + 3 + (*z).Key.Msgsize() + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 2 + msgp.BytesPrefixSize + len((*z).PublicAddress)
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z *identityChallenge) MsgIsZero() bool {
+ return ((*z).Key.MsgIsZero()) && ((*z).Challenge == (identityChallengeValue{})) && (len((*z).PublicAddress) == 0)
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *identityChallengeResponse) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ // omitempty: check for empty values
+ zb0003Len := uint32(3)
+ var zb0003Mask uint8 /* 4 bits */
+ if (*z).Challenge == (identityChallengeValue{}) {
+ zb0003Len--
+ zb0003Mask |= 0x2
+ }
+ if (*z).Key.MsgIsZero() {
+ zb0003Len--
+ zb0003Mask |= 0x4
+ }
+ if (*z).ResponseChallenge == (identityChallengeValue{}) {
+ zb0003Len--
+ zb0003Mask |= 0x8
+ }
+ // variable map header, size zb0003Len
+ o = append(o, 0x80|uint8(zb0003Len))
+ if zb0003Len != 0 {
+ if (zb0003Mask & 0x2) == 0 { // if not empty
+ // string "c"
+ o = append(o, 0xa1, 0x63)
+ o = msgp.AppendBytes(o, ((*z).Challenge)[:])
+ }
+ if (zb0003Mask & 0x4) == 0 { // if not empty
+ // string "pk"
+ o = append(o, 0xa2, 0x70, 0x6b)
+ o = (*z).Key.MarshalMsg(o)
+ }
+ if (zb0003Mask & 0x8) == 0 { // if not empty
+ // string "rc"
+ o = append(o, 0xa2, 0x72, 0x63)
+ o = msgp.AppendBytes(o, ((*z).ResponseChallenge)[:])
+ }
+ }
+ return
+}
+
+func (_ *identityChallengeResponse) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeResponse)
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *identityChallengeResponse) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0003 int
+ var zb0004 bool
+ zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0003, zb0004, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0003 > 0 {
+ zb0003--
+ bts, err = (*z).Key.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Key")
+ return
+ }
+ }
+ if zb0003 > 0 {
+ zb0003--
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Challenge")
+ return
+ }
+ }
+ if zb0003 > 0 {
+ zb0003--
+ bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "ResponseChallenge")
+ return
+ }
+ }
+ if zb0003 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0003)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0004 {
+ (*z) = identityChallengeResponse{}
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch string(field) {
+ case "pk":
+ bts, err = (*z).Key.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ case "c":
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Challenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "Challenge")
+ return
+ }
+ case "rc":
+ bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "ResponseChallenge")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+func (_ *identityChallengeResponse) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeResponse)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *identityChallengeResponse) Msgsize() (s int) {
+ s = 1 + 3 + (*z).Key.Msgsize() + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize))
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z *identityChallengeResponse) MsgIsZero() bool {
+ return ((*z).Key.MsgIsZero()) && ((*z).Challenge == (identityChallengeValue{})) && ((*z).ResponseChallenge == (identityChallengeValue{}))
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *identityChallengeResponseSigned) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ // omitempty: check for empty values
+ zb0001Len := uint32(2)
+ var zb0001Mask uint8 /* 3 bits */
+ if (*z).Msg.MsgIsZero() {
+ zb0001Len--
+ zb0001Mask |= 0x2
+ }
+ if (*z).Signature.MsgIsZero() {
+ zb0001Len--
+ zb0001Mask |= 0x4
+ }
+ // variable map header, size zb0001Len
+ o = append(o, 0x80|uint8(zb0001Len))
+ if zb0001Len != 0 {
+ if (zb0001Mask & 0x2) == 0 { // if not empty
+ // string "icr"
+ o = append(o, 0xa3, 0x69, 0x63, 0x72)
+ o = (*z).Msg.MarshalMsg(o)
+ }
+ if (zb0001Mask & 0x4) == 0 { // if not empty
+ // string "sig"
+ o = append(o, 0xa3, 0x73, 0x69, 0x67)
+ o = (*z).Signature.MarshalMsg(o)
+ }
+ }
+ return
+}
+
+func (_ *identityChallengeResponseSigned) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeResponseSigned)
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *identityChallengeResponseSigned) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 int
+ var zb0002 bool
+ zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 > 0 {
+ zb0001--
+ bts, err = (*z).Msg.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg")
+ return
+ }
+ }
+ if zb0001 > 0 {
+ zb0001--
+ bts, err = (*z).Signature.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Signature")
+ return
+ }
+ }
+ if zb0001 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0001)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0002 {
+ (*z) = identityChallengeResponseSigned{}
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch string(field) {
+ case "icr":
+ bts, err = (*z).Msg.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Msg")
+ return
+ }
+ case "sig":
+ bts, err = (*z).Signature.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Signature")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+func (_ *identityChallengeResponseSigned) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeResponseSigned)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *identityChallengeResponseSigned) Msgsize() (s int) {
+ s = 1 + 4 + (*z).Msg.Msgsize() + 4 + (*z).Signature.Msgsize()
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z *identityChallengeResponseSigned) MsgIsZero() bool {
+ return ((*z).Msg.MsgIsZero()) && ((*z).Signature.MsgIsZero())
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *identityChallengeSigned) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ // omitempty: check for empty values
+ zb0001Len := uint32(2)
+ var zb0001Mask uint8 /* 3 bits */
+ if (*z).Msg.MsgIsZero() {
+ zb0001Len--
+ zb0001Mask |= 0x2
+ }
+ if (*z).Signature.MsgIsZero() {
+ zb0001Len--
+ zb0001Mask |= 0x4
+ }
+ // variable map header, size zb0001Len
+ o = append(o, 0x80|uint8(zb0001Len))
+ if zb0001Len != 0 {
+ if (zb0001Mask & 0x2) == 0 { // if not empty
+ // string "ic"
+ o = append(o, 0xa2, 0x69, 0x63)
+ o = (*z).Msg.MarshalMsg(o)
+ }
+ if (zb0001Mask & 0x4) == 0 { // if not empty
+ // string "sig"
+ o = append(o, 0xa3, 0x73, 0x69, 0x67)
+ o = (*z).Signature.MarshalMsg(o)
+ }
+ }
+ return
+}
+
+func (_ *identityChallengeSigned) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeSigned)
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *identityChallengeSigned) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 int
+ var zb0002 bool
+ zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 > 0 {
+ zb0001--
+ bts, err = (*z).Msg.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg")
+ return
+ }
+ }
+ if zb0001 > 0 {
+ zb0001--
+ bts, err = (*z).Signature.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Signature")
+ return
+ }
+ }
+ if zb0001 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0001)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0002 {
+ (*z) = identityChallengeSigned{}
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch string(field) {
+ case "ic":
+ bts, err = (*z).Msg.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Msg")
+ return
+ }
+ case "sig":
+ bts, err = (*z).Signature.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Signature")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+func (_ *identityChallengeSigned) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeSigned)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *identityChallengeSigned) Msgsize() (s int) {
+ s = 1 + 3 + (*z).Msg.Msgsize() + 4 + (*z).Signature.Msgsize()
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z *identityChallengeSigned) MsgIsZero() bool {
+ return ((*z).Msg.MsgIsZero()) && ((*z).Signature.MsgIsZero())
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *identityChallengeValue) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendBytes(o, (*z)[:])
+ return
+}
+
+func (_ *identityChallengeValue) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeValue)
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *identityChallengeValue) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ bts, err = msgp.ReadExactBytes(bts, (*z)[:])
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ o = bts
+ return
+}
+
+func (_ *identityChallengeValue) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityChallengeValue)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *identityChallengeValue) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize + (32 * (msgp.ByteSize))
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z *identityChallengeValue) MsgIsZero() bool {
+ return (*z) == (identityChallengeValue{})
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *identityVerificationMessage) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ // omitempty: check for empty values
+ zb0002Len := uint32(1)
+ var zb0002Mask uint8 /* 2 bits */
+ if (*z).ResponseChallenge == (identityChallengeValue{}) {
+ zb0002Len--
+ zb0002Mask |= 0x2
+ }
+ // variable map header, size zb0002Len
+ o = append(o, 0x80|uint8(zb0002Len))
+ if zb0002Len != 0 {
+ if (zb0002Mask & 0x2) == 0 { // if not empty
+ // string "rc"
+ o = append(o, 0xa2, 0x72, 0x63)
+ o = msgp.AppendBytes(o, ((*z).ResponseChallenge)[:])
+ }
+ }
+ return
+}
+
+func (_ *identityVerificationMessage) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityVerificationMessage)
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *identityVerificationMessage) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0002 int
+ var zb0003 bool
+ zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0002 > 0 {
+ zb0002--
+ bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "ResponseChallenge")
+ return
+ }
+ }
+ if zb0002 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0002)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0003 {
+ (*z) = identityVerificationMessage{}
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch string(field) {
+ case "rc":
+ bts, err = msgp.ReadExactBytes(bts, ((*z).ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "ResponseChallenge")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+func (_ *identityVerificationMessage) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityVerificationMessage)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *identityVerificationMessage) Msgsize() (s int) {
+ s = 1 + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize))
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z *identityVerificationMessage) MsgIsZero() bool {
+ return ((*z).ResponseChallenge == (identityChallengeValue{}))
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *identityVerificationMessageSigned) MarshalMsg(b []byte) (o []byte) {
+ o = msgp.Require(b, z.Msgsize())
+ // omitempty: check for empty values
+ zb0002Len := uint32(2)
+ var zb0002Mask uint8 /* 3 bits */
+ if (*z).Msg.ResponseChallenge == (identityChallengeValue{}) {
+ zb0002Len--
+ zb0002Mask |= 0x2
+ }
+ if (*z).Signature.MsgIsZero() {
+ zb0002Len--
+ zb0002Mask |= 0x4
+ }
+ // variable map header, size zb0002Len
+ o = append(o, 0x80|uint8(zb0002Len))
+ if zb0002Len != 0 {
+ if (zb0002Mask & 0x2) == 0 { // if not empty
+ // string "ivm"
+ o = append(o, 0xa3, 0x69, 0x76, 0x6d)
+ // omitempty: check for empty values
+ zb0003Len := uint32(1)
+ var zb0003Mask uint8 /* 2 bits */
+ if (*z).Msg.ResponseChallenge == (identityChallengeValue{}) {
+ zb0003Len--
+ zb0003Mask |= 0x2
+ }
+ // variable map header, size zb0003Len
+ o = append(o, 0x80|uint8(zb0003Len))
+ if (zb0003Mask & 0x2) == 0 { // if not empty
+ // string "rc"
+ o = append(o, 0xa2, 0x72, 0x63)
+ o = msgp.AppendBytes(o, ((*z).Msg.ResponseChallenge)[:])
+ }
+ }
+ if (zb0002Mask & 0x4) == 0 { // if not empty
+ // string "sig"
+ o = append(o, 0xa3, 0x73, 0x69, 0x67)
+ o = (*z).Signature.MarshalMsg(o)
+ }
+ }
+ return
+}
+
+func (_ *identityVerificationMessageSigned) CanMarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityVerificationMessageSigned)
+ return ok
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *identityVerificationMessageSigned) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0002 int
+ var zb0003 bool
+ zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0002 > 0 {
+ zb0002--
+ var zb0004 int
+ var zb0005 bool
+ zb0004, zb0005, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg")
+ return
+ }
+ if zb0004 > 0 {
+ zb0004--
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg", "struct-from-array", "ResponseChallenge")
+ return
+ }
+ }
+ if zb0004 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0004)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg", "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg")
+ return
+ }
+ if zb0005 {
+ (*z).Msg = identityVerificationMessage{}
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg")
+ return
+ }
+ switch string(field) {
+ case "rc":
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg", "ResponseChallenge")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Msg")
+ return
+ }
+ }
+ }
+ }
+ }
+ if zb0002 > 0 {
+ zb0002--
+ bts, err = (*z).Signature.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array", "Signature")
+ return
+ }
+ }
+ if zb0002 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0002)
+ if err != nil {
+ err = msgp.WrapError(err, "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0003 {
+ (*z) = identityVerificationMessageSigned{}
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch string(field) {
+ case "ivm":
+ var zb0006 int
+ var zb0007 bool
+ zb0006, zb0007, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if _, ok := err.(msgp.TypeError); ok {
+ zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Msg")
+ return
+ }
+ if zb0006 > 0 {
+ zb0006--
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "Msg", "struct-from-array", "ResponseChallenge")
+ return
+ }
+ }
+ if zb0006 > 0 {
+ err = msgp.ErrTooManyArrayFields(zb0006)
+ if err != nil {
+ err = msgp.WrapError(err, "Msg", "struct-from-array")
+ return
+ }
+ }
+ } else {
+ if err != nil {
+ err = msgp.WrapError(err, "Msg")
+ return
+ }
+ if zb0007 {
+ (*z).Msg = identityVerificationMessage{}
+ }
+ for zb0006 > 0 {
+ zb0006--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Msg")
+ return
+ }
+ switch string(field) {
+ case "rc":
+ bts, err = msgp.ReadExactBytes(bts, ((*z).Msg.ResponseChallenge)[:])
+ if err != nil {
+ err = msgp.WrapError(err, "Msg", "ResponseChallenge")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err, "Msg")
+ return
+ }
+ }
+ }
+ }
+ case "sig":
+ bts, err = (*z).Signature.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Signature")
+ return
+ }
+ default:
+ err = msgp.ErrNoField(string(field))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+func (_ *identityVerificationMessageSigned) CanUnmarshalMsg(z interface{}) bool {
+ _, ok := (z).(*identityVerificationMessageSigned)
+ return ok
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *identityVerificationMessageSigned) Msgsize() (s int) {
+ s = 1 + 4 + 1 + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 4 + (*z).Signature.Msgsize()
+ return
+}
+
+// MsgIsZero returns whether this is a zero value
+func (z *identityVerificationMessageSigned) MsgIsZero() bool {
+ return ((*z).Msg.ResponseChallenge == (identityChallengeValue{})) && ((*z).Signature.MsgIsZero())
+}
diff --git a/network/msgp_gen_test.go b/network/msgp_gen_test.go
new file mode 100644
index 0000000000..1046371a1a
--- /dev/null
+++ b/network/msgp_gen_test.go
@@ -0,0 +1,435 @@
+//go:build !skip_msgp_testing
+// +build !skip_msgp_testing
+
+package network
+
+// Code generated by github.com/algorand/msgp DO NOT EDIT.
+
+import (
+ "testing"
+
+ "github.com/algorand/msgp/msgp"
+
+ "github.com/algorand/go-algorand/protocol"
+ "github.com/algorand/go-algorand/test/partitiontest"
+)
+
+func TestMarshalUnmarshalidentityChallenge(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ v := identityChallenge{}
+ bts := v.MarshalMsg(nil)
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func TestRandomizedEncodingidentityChallenge(t *testing.T) {
+ protocol.RunEncodingTest(t, &identityChallenge{})
+}
+
+func BenchmarkMarshalMsgidentityChallenge(b *testing.B) {
+ v := identityChallenge{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgidentityChallenge(b *testing.B) {
+ v := identityChallenge{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalidentityChallenge(b *testing.B) {
+ v := identityChallenge{}
+ bts := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalidentityChallengeResponse(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ v := identityChallengeResponse{}
+ bts := v.MarshalMsg(nil)
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func TestRandomizedEncodingidentityChallengeResponse(t *testing.T) {
+ protocol.RunEncodingTest(t, &identityChallengeResponse{})
+}
+
+func BenchmarkMarshalMsgidentityChallengeResponse(b *testing.B) {
+ v := identityChallengeResponse{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgidentityChallengeResponse(b *testing.B) {
+ v := identityChallengeResponse{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalidentityChallengeResponse(b *testing.B) {
+ v := identityChallengeResponse{}
+ bts := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalidentityChallengeResponseSigned(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ v := identityChallengeResponseSigned{}
+ bts := v.MarshalMsg(nil)
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func TestRandomizedEncodingidentityChallengeResponseSigned(t *testing.T) {
+ protocol.RunEncodingTest(t, &identityChallengeResponseSigned{})
+}
+
+func BenchmarkMarshalMsgidentityChallengeResponseSigned(b *testing.B) {
+ v := identityChallengeResponseSigned{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgidentityChallengeResponseSigned(b *testing.B) {
+ v := identityChallengeResponseSigned{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalidentityChallengeResponseSigned(b *testing.B) {
+ v := identityChallengeResponseSigned{}
+ bts := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalidentityChallengeSigned(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ v := identityChallengeSigned{}
+ bts := v.MarshalMsg(nil)
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func TestRandomizedEncodingidentityChallengeSigned(t *testing.T) {
+ protocol.RunEncodingTest(t, &identityChallengeSigned{})
+}
+
+func BenchmarkMarshalMsgidentityChallengeSigned(b *testing.B) {
+ v := identityChallengeSigned{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgidentityChallengeSigned(b *testing.B) {
+ v := identityChallengeSigned{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalidentityChallengeSigned(b *testing.B) {
+ v := identityChallengeSigned{}
+ bts := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalidentityChallengeValue(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ v := identityChallengeValue{}
+ bts := v.MarshalMsg(nil)
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func TestRandomizedEncodingidentityChallengeValue(t *testing.T) {
+ protocol.RunEncodingTest(t, &identityChallengeValue{})
+}
+
+func BenchmarkMarshalMsgidentityChallengeValue(b *testing.B) {
+ v := identityChallengeValue{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgidentityChallengeValue(b *testing.B) {
+ v := identityChallengeValue{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalidentityChallengeValue(b *testing.B) {
+ v := identityChallengeValue{}
+ bts := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalidentityVerificationMessage(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ v := identityVerificationMessage{}
+ bts := v.MarshalMsg(nil)
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func TestRandomizedEncodingidentityVerificationMessage(t *testing.T) {
+ protocol.RunEncodingTest(t, &identityVerificationMessage{})
+}
+
+func BenchmarkMarshalMsgidentityVerificationMessage(b *testing.B) {
+ v := identityVerificationMessage{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgidentityVerificationMessage(b *testing.B) {
+ v := identityVerificationMessage{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalidentityVerificationMessage(b *testing.B) {
+ v := identityVerificationMessage{}
+ bts := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalidentityVerificationMessageSigned(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ v := identityVerificationMessageSigned{}
+ bts := v.MarshalMsg(nil)
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func TestRandomizedEncodingidentityVerificationMessageSigned(t *testing.T) {
+ protocol.RunEncodingTest(t, &identityVerificationMessageSigned{})
+}
+
+func BenchmarkMarshalMsgidentityVerificationMessageSigned(b *testing.B) {
+ v := identityVerificationMessageSigned{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgidentityVerificationMessageSigned(b *testing.B) {
+ v := identityVerificationMessageSigned{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalidentityVerificationMessageSigned(b *testing.B) {
+ v := identityVerificationMessageSigned{}
+ bts := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/network/netidentity.go b/network/netidentity.go
new file mode 100644
index 0000000000..dd8abceff0
--- /dev/null
+++ b/network/netidentity.go
@@ -0,0 +1,412 @@
+// Copyright (C) 2019-2023 Algorand, Inc.
+// This file is part of go-algorand
+//
+// go-algorand is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// go-algorand is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with go-algorand. If not, see .
+
+package network
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "sync/atomic"
+
+ "github.com/algorand/go-algorand/crypto"
+ "github.com/algorand/go-algorand/protocol"
+)
+
+// netidentity.go implements functionality to participate in an "Identity Challenge Exchange"
+// with the purpose of identifying redundant connections between peers, and preventing them.
+// The identity challenge exchange protocol is a 3 way handshake that exchanges signed messages.
+//
+// Message 1 (Identity Challenge): when a request is made to start a gossip connection, an
+// identityChallengeSigned message is added to HTTP request headers, containing:
+// - a 32 byte random challenge
+// - the requester's "identity" PublicKey
+// - the PublicAddress of the intended recipient
+// - Signature on the above by the requester's PublicKey
+//
+// Message 2 (Identity Challenge Response): when responding to the gossip connection request,
+// if the identity challenge is valid, an identityChallengeResponseSigned message is added
+// to the HTTP response headers, containing:
+// - the original 32 byte random challenge from Message 1
+// - a new "response" 32 byte random challenge
+// - the responder's "identity" PublicKey
+// - Signature on the above by the responder's PublicKey
+//
+// Message 3 (Identity Verification): if the identityChallengeResponse is valid, the requester
+// sends a NetIDVerificationTag message over websockets to verify it owns its PublicKey, with:
+// - Signature on the response challenge from Message 2, using the requester's PublicKey
+//
+// Upon receipt of Message 2, the requester has enough data to consider the responder's identity "verified".
+// Upon receipt of Message 3, the responder has enough data to consider the requester's identity "verified".
+// At each of these steps, if the peer's identity was verified, wsNetwork will attempt to add it to the
+// identityTracker, which maintains a single peer per identity PublicKey. If the identity is already in use
+// by another connected peer, we know this connection is a duplicate, and can be closed.
+//
+// Protocol Enablement:
+// This exchange is optional, and is enabled by setting the configuration value "PublicAddress" to match the
+// node's public endpoint address stored in other peers' phonebooks (like "r-aa.algorand-mainnet.network:4160").
+//
+// Protocol Error Handling:
+// Message 1
+// - If the Message is not included, assume the peer does not use identity exchange, and peer without attaching an identityChallengeResponse
+// - If the Address included in the challenge is not this node's PublicAddress, peering continues without identity exchange.
+// this is so that if an operator misconfigures PublicAddress, it does not decline well meaning peering attempts
+// - If the Message is malformed or cannot be decoded, the peering attempt is stopped
+// - If the Signature in the challenge does not verify to the included key, the peering attempt is stopped
+//
+// Message 2
+// - If the Message is not included, assume the peer does not use identity exchange, and do not send Message 3
+// - If the Message is malformed or cannot be decoded, the peering attempt is stopped
+// - If the original 32 byte challenge does not match the one sent in Message 1, the peering attempt is stopped
+// - If the Signature in the challenge does not verify to the included key, the peering attempt is stopped
+//
+// Message 3
+// - If the Message is malformed or cannot be decoded, the peer is disconnected
+// - If the Signature in the challenge does not verify peer's assumed PublicKey and assigned Challenge Bytes, the peer is disconnected
+// - If the Message is not received, no action is taken to disconnect the peer.
+
+const maxAddressLen = 256 + 32 // Max DNS (255) + margin for port specification
+
+// identityChallengeValue is 32 random bytes used for identity challenge exchange
+type identityChallengeValue [32]byte
+
+func newIdentityChallengeValue() identityChallengeValue {
+ var ret identityChallengeValue
+ crypto.RandBytes(ret[:])
+ return ret
+}
+
+type identityChallengeScheme interface {
+ AttachChallenge(attachTo http.Header, addr string) identityChallengeValue
+ VerifyRequestAndAttachResponse(attachTo http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error)
+ VerifyResponse(h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error)
+}
+
+// identityChallengePublicKeyScheme implements IdentityChallengeScheme by
+// exchanging and verifying public key challenges and attaching them to headers,
+// or returning the message payload to be sent
+type identityChallengePublicKeyScheme struct {
+ dedupName string
+ identityKeys *crypto.SignatureSecrets
+}
+
+// NewIdentityChallengeScheme will create a default Identification Scheme
+func NewIdentityChallengeScheme(dn string) *identityChallengePublicKeyScheme {
+ // without an deduplication name, there is no identityto manage, so just return an empty scheme
+ if dn == "" {
+ return &identityChallengePublicKeyScheme{}
+ }
+ var seed crypto.Seed
+ crypto.RandBytes(seed[:])
+
+ return &identityChallengePublicKeyScheme{
+ dedupName: dn,
+ identityKeys: crypto.GenerateSignatureSecrets(seed),
+ }
+}
+
+// AttachChallenge will generate a new identity challenge and will encode and attach the challenge
+// as a header. It returns the identityChallengeValue used for this challenge, so the network can
+// confirm it later (by passing it to VerifyResponse), or returns an empty challenge if dedupName is
+// not set.
+func (i identityChallengePublicKeyScheme) AttachChallenge(attachTo http.Header, addr string) identityChallengeValue {
+ if i.dedupName == "" || addr == "" {
+ return identityChallengeValue{}
+ }
+ c := identityChallenge{
+ Key: i.identityKeys.SignatureVerifier,
+ Challenge: newIdentityChallengeValue(),
+ PublicAddress: []byte(addr),
+ }
+
+ attachTo.Add(IdentityChallengeHeader, c.signAndEncodeB64(i.identityKeys))
+ return c.Challenge
+}
+
+// VerifyRequestAndAttachResponse checks headers for an Identity Challenge, and verifies:
+// * the provided challenge bytes matches the one encoded in the header
+// * the identity challenge verifies against the included key
+// * the "Address" field matches what this scheme expects
+// once verified, it will attach the header to the "attach" header
+// and will return the challenge and identity of the peer for recording
+// or returns empty values if the header did not end up getting set
+func (i identityChallengePublicKeyScheme) VerifyRequestAndAttachResponse(attachTo http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) {
+ // if dedupName is not set, this scheme is not configured to exchange identity
+ if i.dedupName == "" {
+ return identityChallengeValue{}, crypto.PublicKey{}, nil
+ }
+ // if the headerString is not populated, the peer isn't participating in identity exchange
+ headerString := h.Get(IdentityChallengeHeader)
+ if headerString == "" {
+ return identityChallengeValue{}, crypto.PublicKey{}, nil
+ }
+ // decode the header to an identityChallenge
+ msg, err := base64.StdEncoding.DecodeString(headerString)
+ if err != nil {
+ return identityChallengeValue{}, crypto.PublicKey{}, err
+ }
+ idChal := identityChallengeSigned{}
+ err = protocol.Decode(msg, &idChal)
+ if err != nil {
+ return identityChallengeValue{}, crypto.PublicKey{}, err
+ }
+ if !idChal.Verify() {
+ return identityChallengeValue{}, crypto.PublicKey{}, fmt.Errorf("identity challenge incorrectly signed")
+ }
+ // if the address is not meant for this host, return without attaching headers,
+ // but also do not emit an error. This is because if an operator were to incorrectly
+ // specify their dedupName, it could result in inappropriate disconnections from valid peers
+ if string(idChal.Msg.PublicAddress) != i.dedupName {
+ return identityChallengeValue{}, crypto.PublicKey{}, nil
+ }
+ // make the response object, encode it and attach it to the header
+ r := identityChallengeResponse{
+ Key: i.identityKeys.SignatureVerifier,
+ Challenge: idChal.Msg.Challenge,
+ ResponseChallenge: newIdentityChallengeValue(),
+ }
+ attachTo.Add(IdentityChallengeHeader, r.signAndEncodeB64(i.identityKeys))
+ return r.ResponseChallenge, idChal.Msg.Key, nil
+}
+
+// VerifyResponse will decode the identity challenge header from an HTTP response (containing an
+// encoding of identityChallengeResponseSigned) and confirm it has a valid signature, and that the
+// provided challenge (generated and added to the HTTP request by AttachChallenge) matches the one
+// found in the header. If the response can be verified, it returns the identity of the peer and an
+// encoded identityVerificationMessage to send to the peer. Otherwise, it returns empty values.
+func (i identityChallengePublicKeyScheme) VerifyResponse(h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) {
+ // if we are not participating in identity challenge exchange, do nothing (no error and no value)
+ if i.dedupName == "" {
+ return crypto.PublicKey{}, []byte{}, nil
+ }
+ headerString := h.Get(IdentityChallengeHeader)
+ // if the header is not populated, assume the peer is not participating in identity exchange
+ if headerString == "" {
+ return crypto.PublicKey{}, []byte{}, nil
+ }
+ msg, err := base64.StdEncoding.DecodeString(headerString)
+ if err != nil {
+ return crypto.PublicKey{}, []byte{}, err
+ }
+ resp := identityChallengeResponseSigned{}
+ err = protocol.Decode(msg, &resp)
+ if err != nil {
+ return crypto.PublicKey{}, []byte{}, err
+ }
+ if resp.Msg.Challenge != c {
+ return crypto.PublicKey{}, []byte{}, fmt.Errorf("challenge response did not contain originally issued challenge value")
+ }
+ if !resp.Verify() {
+ return crypto.PublicKey{}, []byte{}, fmt.Errorf("challenge response incorrectly signed ")
+ }
+ return resp.Msg.Key, i.identityVerificationMessage(resp.Msg.ResponseChallenge), nil
+}
+
+// identityVerificationMessage generates the 3rd message of the challenge exchange,
+// which a wsNetwork can then send to a peer in order to verify their own identity.
+// It is prefixed with the ID Verification tag and returned ready-to-send
+func (i *identityChallengePublicKeyScheme) identityVerificationMessage(c identityChallengeValue) []byte {
+ signedMsg := identityVerificationMessage{ResponseChallenge: c}.Sign(i.identityKeys)
+ return append([]byte(protocol.NetIDVerificationTag), protocol.Encode(&signedMsg)...)
+}
+
+// The initial challenge object, giving the peer a challenge to return (Challenge),
+// the presumed identity of this node (Key), the intended recipient (Address).
+type identityChallenge struct {
+ _struct struct{} `codec:",omitempty,omitemptyarray"`
+
+ Key crypto.PublicKey `codec:"pk"`
+ Challenge identityChallengeValue `codec:"c"`
+ PublicAddress []byte `codec:"a,allocbound=maxAddressLen"`
+}
+
+// identityChallengeSigned wraps an identityChallenge with a signature, similar to SignedTxn and
+// netPrioResponseSigned.
+type identityChallengeSigned struct {
+ _struct struct{} `codec:",omitempty,omitemptyarray"`
+
+ Msg identityChallenge `codec:"ic"`
+ Signature crypto.Signature `codec:"sig"`
+}
+
+// The response to an identityChallenge, containing the responder's public key, the original
+// requestor's challenge, and a new challenge for the requestor.
+type identityChallengeResponse struct {
+ _struct struct{} `codec:",omitempty,omitemptyarray"`
+
+ Key crypto.PublicKey `codec:"pk"`
+ Challenge identityChallengeValue `codec:"c"`
+ ResponseChallenge identityChallengeValue `codec:"rc"`
+}
+
+type identityChallengeResponseSigned struct {
+ _struct struct{} `codec:",omitempty,omitemptyarray"`
+
+ Msg identityChallengeResponse `codec:"icr"`
+ Signature crypto.Signature `codec:"sig"`
+}
+
+type identityVerificationMessage struct {
+ _struct struct{} `codec:",omitempty,omitemptyarray"`
+
+ ResponseChallenge identityChallengeValue `codec:"rc"`
+}
+
+type identityVerificationMessageSigned struct {
+ _struct struct{} `codec:",omitempty,omitemptyarray"`
+
+ Msg identityVerificationMessage `codec:"ivm"`
+ Signature crypto.Signature `codec:"sig"`
+}
+
+func (i identityChallenge) signAndEncodeB64(s *crypto.SignatureSecrets) string {
+ signedChal := i.Sign(s)
+ return base64.StdEncoding.EncodeToString(protocol.Encode(&signedChal))
+}
+
+func (i identityChallenge) Sign(secrets *crypto.SignatureSecrets) identityChallengeSigned {
+ return identityChallengeSigned{Msg: i, Signature: secrets.Sign(i)}
+}
+
+func (i identityChallenge) ToBeHashed() (protocol.HashID, []byte) {
+ return protocol.NetIdentityChallenge, protocol.Encode(&i)
+}
+
+// Verify checks that the signature included in the identityChallenge was indeed created by the included Key
+func (i identityChallengeSigned) Verify() bool {
+ return i.Msg.Key.Verify(i.Msg, i.Signature)
+}
+
+func (i identityChallengeResponse) signAndEncodeB64(s *crypto.SignatureSecrets) string {
+ signedChalResp := i.Sign(s)
+ return base64.StdEncoding.EncodeToString(protocol.Encode(&signedChalResp))
+}
+
+func (i identityChallengeResponse) Sign(secrets *crypto.SignatureSecrets) identityChallengeResponseSigned {
+ return identityChallengeResponseSigned{Msg: i, Signature: secrets.Sign(i)}
+}
+
+func (i identityChallengeResponse) ToBeHashed() (protocol.HashID, []byte) {
+ return protocol.NetIdentityChallengeResponse, protocol.Encode(&i)
+}
+
+// Verify checks that the signature included in the identityChallengeResponse was indeed created by the included Key
+func (i identityChallengeResponseSigned) Verify() bool {
+ return i.Msg.Key.Verify(i.Msg, i.Signature)
+}
+
+func (i identityVerificationMessage) Sign(secrets *crypto.SignatureSecrets) identityVerificationMessageSigned {
+ return identityVerificationMessageSigned{Msg: i, Signature: secrets.Sign(i)}
+}
+
+func (i identityVerificationMessage) ToBeHashed() (protocol.HashID, []byte) {
+ return protocol.NetIdentityVerificationMessage, protocol.Encode(&i)
+}
+
+// Verify checks that the signature included in the identityVerificationMessage was indeed created by the included Key
+func (i identityVerificationMessageSigned) Verify(key crypto.PublicKey) bool {
+ return key.Verify(i.Msg, i.Signature)
+}
+
+// identityVerificationHandler receives a signature over websocket, and confirms it matches the
+// sender's claimed identity and the challenge that was assigned to it. If the identity is available,
+// the peer is loaded into the identity tracker. Otherwise, we ask the network to disconnect the peer.
+func identityVerificationHandler(message IncomingMessage) OutgoingMessage {
+ peer := message.Sender.(*wsPeer)
+ // avoid doing work (crypto and potentially taking a lock) if the peer is already verified
+ if atomic.LoadUint32(&peer.identityVerified) == 1 {
+ return OutgoingMessage{}
+ }
+ localAddr, _ := peer.net.Address()
+ msg := identityVerificationMessageSigned{}
+ err := protocol.Decode(message.Data, &msg)
+ if err != nil {
+ networkPeerIdentityError.Inc(nil)
+ peer.net.log.With("err", err).With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity verification could not be decoded, disconnecting")
+ peer.net.disconnect(peer, disconnectBadIdentityData)
+ return OutgoingMessage{}
+ }
+ if peer.identityChallenge != msg.Msg.ResponseChallenge {
+ networkPeerIdentityError.Inc(nil)
+ peer.net.log.With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity verification challenge does not match, disconnecting")
+ peer.net.disconnect(peer, disconnectBadIdentityData)
+ return OutgoingMessage{}
+ }
+ if !msg.Verify(peer.identity) {
+ networkPeerIdentityError.Inc(nil)
+ peer.net.log.With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity verification is incorrectly signed, disconnecting")
+ peer.net.disconnect(peer, disconnectBadIdentityData)
+ return OutgoingMessage{}
+ }
+ atomic.StoreUint32(&peer.identityVerified, 1)
+ // if the identity could not be claimed by this peer, it means the identity is in use
+ peer.net.peersLock.Lock()
+ ok := peer.net.identityTracker.setIdentity(peer)
+ peer.net.peersLock.Unlock()
+ if !ok {
+ networkPeerIdentityDisconnect.Inc(nil)
+ peer.net.log.With("remote", peer.OriginAddress()).With("local", localAddr).Warn("peer identity already in use, disconnecting")
+ peer.net.disconnect(peer, disconnectDuplicateConnection)
+ }
+ return OutgoingMessage{}
+}
+
+var identityHandlers = []TaggedMessageHandler{
+ {protocol.NetIDVerificationTag, HandlerFunc(identityVerificationHandler)},
+}
+
+// identityTracker is used by wsNetwork to manage peer identities for connection deduplication
+type identityTracker interface {
+ removeIdentity(p *wsPeer)
+ setIdentity(p *wsPeer) bool
+}
+
+// publicKeyIdentTracker implements identityTracker by
+// mapping from PublicKeys exchanged in identity challenges to a peer
+// this structure is not thread-safe; it is protected by wn.peersLock.
+type publicKeyIdentTracker struct {
+ peersByID map[crypto.PublicKey]*wsPeer
+}
+
+// NewIdentityTracker returns a new publicKeyIdentTracker
+func NewIdentityTracker() *publicKeyIdentTracker {
+ return &publicKeyIdentTracker{
+ peersByID: make(map[crypto.PublicKey]*wsPeer),
+ }
+}
+
+// setIdentity attempts to store a peer at its identity.
+// returns false if it was unable to load the peer into the given identity
+// or true otherwise (if the peer was already there, or if it was added)
+func (t *publicKeyIdentTracker) setIdentity(p *wsPeer) bool {
+ existingPeer, exists := t.peersByID[p.identity]
+ if !exists {
+ // the identity is not occupied, so set it and return true
+ t.peersByID[p.identity] = p
+ return true
+ }
+ // the identity is occupied, so return false if it is occupied by some *other* peer
+ // or true if it is occupied by this peer
+ return existingPeer == p
+}
+
+// removeIdentity removes the entry in the peersByID map if it exists
+// and is occupied by the given peer
+func (t *publicKeyIdentTracker) removeIdentity(p *wsPeer) {
+ if t.peersByID[p.identity] == p {
+ delete(t.peersByID, p.identity)
+ }
+}
diff --git a/network/netidentity_test.go b/network/netidentity_test.go
new file mode 100644
index 0000000000..f3c72e3e8c
--- /dev/null
+++ b/network/netidentity_test.go
@@ -0,0 +1,366 @@
+// Copyright (C) 2019-2023 Algorand, Inc.
+// This file is part of go-algorand
+//
+// go-algorand is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// go-algorand is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with go-algorand. If not, see .
+
+package network
+
+import (
+ "encoding/base64"
+ "net/http"
+ "testing"
+
+ "github.com/algorand/go-algorand/crypto"
+ "github.com/algorand/go-algorand/protocol"
+ "github.com/algorand/go-algorand/test/partitiontest"
+ "github.com/stretchr/testify/require"
+)
+
+// if the scheme has a dedup name, attach to headers. otherwise, don't
+func TestIdentityChallengeSchemeAttachIfEnabled(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("")
+ chal := i.AttachChallenge(h, "other")
+ require.Empty(t, h.Get(IdentityChallengeHeader))
+ require.Empty(t, chal)
+
+ j := NewIdentityChallengeScheme("yes")
+ chal = j.AttachChallenge(h, "other")
+ require.NotEmpty(t, h.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, chal)
+}
+
+// TestIdentityChallengeSchemeVerifyRequestAndAttachResponse will confirm that the scheme
+// attaches responses only if dedup name is set and the provided challenge verifies
+func TestIdentityChallengeSchemeVerifyRequestAndAttachResponse(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ i := NewIdentityChallengeScheme("i1")
+ // author a challenge to the other scheme
+ h := http.Header{}
+ i.AttachChallenge(h, "i2")
+ require.NotEmpty(t, h.Get(IdentityChallengeHeader))
+
+ // without a dedup name, no response and no error
+ h = http.Header{}
+ i.AttachChallenge(h, "i2")
+ r := http.Header{}
+ i2 := NewIdentityChallengeScheme("")
+ chal, key, err := i2.VerifyRequestAndAttachResponse(r, h)
+ require.Empty(t, r.Get(IdentityChallengeHeader))
+ require.Empty(t, chal)
+ require.Empty(t, key)
+ require.NoError(t, err)
+
+ // if dedup name doesn't match, no response and no error
+ h = http.Header{}
+ i.AttachChallenge(h, "i2")
+ r = http.Header{}
+ i2 = NewIdentityChallengeScheme("not i2")
+ chal, key, err = i2.VerifyRequestAndAttachResponse(r, h)
+ require.Empty(t, r.Get(IdentityChallengeHeader))
+ require.Empty(t, chal)
+ require.Empty(t, key)
+ require.NoError(t, err)
+
+ // if the challenge can't be decoded or verified, error
+ h = http.Header{}
+ h.Add(IdentityChallengeHeader, "garbage")
+ r = http.Header{}
+ i2 = NewIdentityChallengeScheme("i2")
+ chal, key, err = i2.VerifyRequestAndAttachResponse(r, h)
+ require.Empty(t, r.Get(IdentityChallengeHeader))
+ require.Empty(t, chal)
+ require.Empty(t, key)
+ require.Error(t, err)
+
+ // happy path: response should be attached here
+ h = http.Header{}
+ i.AttachChallenge(h, "i2")
+ r = http.Header{}
+ i2 = NewIdentityChallengeScheme("i2")
+ chal, key, err = i2.VerifyRequestAndAttachResponse(r, h)
+ require.NotEmpty(t, r.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, chal)
+ require.NotEmpty(t, key)
+ require.NoError(t, err)
+}
+
+func TestIdentityChallengeNoErrorWhenNotParticipating(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ // blank deduplication name will make the scheme a no-op
+ iNotParticipate := NewIdentityChallengeScheme("")
+
+ // create a request header first
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("i1")
+ origChal := i.AttachChallenge(h, "i1")
+ require.NotEmpty(t, h.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, origChal)
+
+ // confirm a nil scheme will not return values or error
+ c, k, err := iNotParticipate.VerifyRequestAndAttachResponse(http.Header{}, h)
+ require.Empty(t, c)
+ require.Empty(t, k)
+ require.NoError(t, err)
+
+ // create a response
+ h2 := http.Header{}
+ i2 := NewIdentityChallengeScheme("i2")
+ i2.VerifyRequestAndAttachResponse(h2, h)
+
+ // confirm a nil scheme will not return values or error
+ k2, bytes, err := iNotParticipate.VerifyResponse(h2, identityChallengeValue{})
+ require.Empty(t, k2)
+ require.Empty(t, bytes)
+ require.NoError(t, err)
+
+ // add broken payload to a new header and try inspecting it with the empty scheme
+ h3 := http.Header{}
+ h3.Add(IdentityChallengeHeader, "broken text!")
+ c, k, err = iNotParticipate.VerifyRequestAndAttachResponse(http.Header{}, h)
+ require.Empty(t, c)
+ require.Empty(t, k)
+ require.NoError(t, err)
+ k2, bytes, err = iNotParticipate.VerifyResponse(h2, identityChallengeValue{})
+ require.Empty(t, k2)
+ require.Empty(t, bytes)
+ require.NoError(t, err)
+}
+
+// TestIdentityChallengeSchemeVerifyResponse confirms the scheme will
+// attach responses only if dedup name is set and the provided challenge verifies
+func TestIdentityChallengeSchemeVerifyResponse(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("i1")
+ // author a challenge to ourselves
+ origChal := i.AttachChallenge(h, "i1")
+ require.NotEmpty(t, h.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, origChal)
+ r := http.Header{}
+
+ respChal, key, err := i.VerifyRequestAndAttachResponse(r, h)
+ require.NotEmpty(t, r.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, respChal)
+ require.NotEmpty(t, key)
+ require.NoError(t, err)
+
+ // respChal2 should match respChal as it is being passed back to the original peer
+ // while origChal will be used for verification
+ key2, verificationMsg, err := i.VerifyResponse(r, origChal)
+ require.NotEmpty(t, verificationMsg)
+ require.NoError(t, err)
+ // because we sent this to ourselves, we can confirm the keys match
+ require.Equal(t, key, key2)
+}
+
+// TestIdentityChallengeSchemeBadSignature tests that the scheme will
+// fail to verify and attach if the challenge is incorrectly signed
+func TestIdentityChallengeSchemeBadSignature(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("i1")
+ // Copy the logic of attaching the header and signing so we can sign it wrong
+ c := identityChallengeSigned{
+ Msg: identityChallenge{
+ Key: i.identityKeys.SignatureVerifier,
+ Challenge: newIdentityChallengeValue(),
+ PublicAddress: []byte("i1"),
+ }}
+ c.Signature = i.identityKeys.SignBytes([]byte("WRONG BYTES SIGNED"))
+ enc := protocol.Encode(&c)
+ b64enc := base64.StdEncoding.EncodeToString(enc)
+ h.Add(IdentityChallengeHeader, b64enc)
+
+ // observe that VerifyRequestAndAttachResponse returns error on bad signature
+ r := http.Header{}
+ respChal, key, err := i.VerifyRequestAndAttachResponse(r, h)
+ require.Empty(t, r.Get(IdentityChallengeHeader))
+ require.Empty(t, respChal)
+ require.Empty(t, key)
+ require.Error(t, err)
+}
+
+// TestIdentityChallengeSchemeBadPayload tests that the scheme will
+// fail to verify if the challenge can't be B64 decoded
+func TestIdentityChallengeSchemeBadPayload(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("i1")
+ h.Add(IdentityChallengeHeader, "NOT VALID BASE 64! :)")
+
+ // observe that VerifyRequestAndAttachResponse won't do anything on bad signature
+ r := http.Header{}
+ respChal, key, err := i.VerifyRequestAndAttachResponse(r, h)
+ require.Empty(t, r.Get(IdentityChallengeHeader))
+ require.Empty(t, respChal)
+ require.Empty(t, key)
+ require.Error(t, err)
+}
+
+// TestIdentityChallengeSchemeBadResponseSignature tests that the scheme will
+// fail to verify if the challenge response is incorrectly signed
+func TestIdentityChallengeSchemeBadResponseSignature(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("i1")
+ // author a challenge to ourselves
+ origChal := i.AttachChallenge(h, "i1")
+ require.NotEmpty(t, h.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, origChal)
+
+ // use the code to sign and encode responses so we can sign incorrectly
+ r := http.Header{}
+ resp := identityChallengeResponseSigned{
+ Msg: identityChallengeResponse{
+ Key: i.identityKeys.SignatureVerifier,
+ Challenge: origChal,
+ ResponseChallenge: newIdentityChallengeValue(),
+ }}
+ resp.Signature = i.identityKeys.SignBytes([]byte("BAD BYTES FOR SIGNING"))
+ enc := protocol.Encode(&resp)
+ b64enc := base64.StdEncoding.EncodeToString(enc)
+ r.Add(IdentityChallengeHeader, b64enc)
+
+ key2, verificationMsg, err := i.VerifyResponse(r, origChal)
+ require.Empty(t, key2)
+ require.Empty(t, verificationMsg)
+ require.Error(t, err)
+}
+
+// TestIdentityChallengeSchemeBadResponsePayload tests that the scheme will
+// fail to verify if the challenge response can't be B64 decoded
+func TestIdentityChallengeSchemeBadResponsePayload(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("i1")
+ // author a challenge to ourselves
+ origChal := i.AttachChallenge(h, "i1")
+ require.NotEmpty(t, h.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, origChal)
+
+ // generate a bad payload that should not decode
+ r := http.Header{}
+ r.Add(IdentityChallengeHeader, "BAD B64 ENCODING :)")
+
+ key2, verificationMsg, err := i.VerifyResponse(r, origChal)
+ require.Empty(t, key2)
+ require.Empty(t, verificationMsg)
+ require.Error(t, err)
+}
+
+// TestIdentityChallengeSchemeWrongChallenge the scheme will
+// return "0" if the challenge does not match upon return
+func TestIdentityChallengeSchemeWrongChallenge(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ h := http.Header{}
+ i := NewIdentityChallengeScheme("i1")
+ // author a challenge to ourselves
+ origChal := i.AttachChallenge(h, "i1")
+ require.NotEmpty(t, h.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, origChal)
+
+ r := http.Header{}
+ respChal, key, err := i.VerifyRequestAndAttachResponse(r, h)
+ require.NotEmpty(t, r.Get(IdentityChallengeHeader))
+ require.NotEmpty(t, respChal)
+ require.NotEmpty(t, key)
+ require.NoError(t, err)
+
+ // Attempt to verify against the wrong challenge
+ key2, verificationMsg, err := i.VerifyResponse(r, newIdentityChallengeValue())
+ require.Empty(t, key2)
+ require.Empty(t, verificationMsg)
+ require.Error(t, err)
+}
+
+func TestNewIdentityTracker(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ tracker := NewIdentityTracker()
+ require.Empty(t, tracker.peersByID)
+}
+
+func TestIdentityTrackerRemoveIdentity(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ tracker := NewIdentityTracker()
+ id := crypto.PublicKey{}
+ p := wsPeer{identity: id}
+
+ id2 := crypto.PublicKey{}
+ p2 := wsPeer{identity: id2}
+
+ // Ensure the first attempt to insert populates the map
+ _, exists := tracker.peersByID[p.identity]
+ require.False(t, exists)
+ require.True(t, tracker.setIdentity(&p))
+ _, exists = tracker.peersByID[p.identity]
+ require.True(t, exists)
+
+ // check that removing a peer who does not exist in the map (but whos identity does)
+ // not not result in the wrong peer being removed
+ tracker.removeIdentity(&p2)
+ _, exists = tracker.peersByID[p.identity]
+ require.True(t, exists)
+
+ tracker.removeIdentity(&p)
+ _, exists = tracker.peersByID[p.identity]
+ require.False(t, exists)
+}
+
+func TestIdentityTrackerSetIdentity(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ tracker := NewIdentityTracker()
+ id := crypto.PublicKey{}
+ p := wsPeer{identity: id}
+
+ // Ensure the first attempt to insert populates the map
+ _, exists := tracker.peersByID[p.identity]
+ require.False(t, exists)
+ require.True(t, tracker.setIdentity(&p))
+ _, exists = tracker.peersByID[p.identity]
+ require.True(t, exists)
+
+ // Ensure the next attempt to insert also returns true
+ require.True(t, tracker.setIdentity(&p))
+
+ // Ensure a different peer cannot take the map entry
+ otherP := wsPeer{identity: id}
+ require.False(t, tracker.setIdentity(&otherP))
+
+ // Ensure the entry in the map wasn't changed
+ require.Equal(t, tracker.peersByID[p.identity], &p)
+}
+
+// Just tests that if a peer is already verified, it just returns OutgoingMessage{}
+func TestHandlerGuard(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ p := wsPeer{identityVerified: uint32(1)}
+ msg := IncomingMessage{
+ Sender: &p,
+ }
+ require.Equal(t, OutgoingMessage{}, identityVerificationHandler(msg))
+}
diff --git a/network/phonebook.go b/network/phonebook.go
index 1ff3ed542d..ac5914a299 100644
--- a/network/phonebook.go
+++ b/network/phonebook.go
@@ -30,6 +30,8 @@ const getAllAddresses = math.MaxInt32
// PhoneBookEntryRoles defines the roles that a single entry on the phonebook can take.
// currently, we have two roles : relay role and archiver role, which are mutually exclusive.
+//
+//msgp:ignore PhoneBookEntryRoles
type PhoneBookEntryRoles int
// PhoneBookEntryRelayRole used for all the relays that are provided either via the algobootstrap SRV record
diff --git a/network/requestTracker.go b/network/requestTracker.go
index d2f05d8bf6..025d75a5a6 100644
--- a/network/requestTracker.go
+++ b/network/requestTracker.go
@@ -148,6 +148,7 @@ func (ard *hostIncomingRequests) countConnections(rateLimitingWindowStartTime ti
return uint(len(ard.requests) - i + len(ard.additionalHostRequests))
}
+//msgp:ignore hostsIncomingMap
type hostsIncomingMap map[string]*hostIncomingRequests
// pruneRequests cleans stale items from the hostRequests maps
diff --git a/network/topics.go b/network/topics.go
index 762312585d..ded264a0cb 100644
--- a/network/topics.go
+++ b/network/topics.go
@@ -30,6 +30,8 @@ const (
)
// Topic is a key-value pair
+//
+//msgp:ignore Topic
type Topic struct {
key string
data []byte
@@ -43,6 +45,8 @@ func MakeTopic(key string, data []byte) Topic {
// Topics is an array of type Topic
// The maximum number of topics allowed is 32
// Each topic key can be 64 characters long and cannot be size 0
+//
+//msgp:ignore Topics
type Topics []Topic
// MarshallTopics serializes the topics into a byte array
diff --git a/network/wsNetwork.go b/network/wsNetwork.go
index e8c9e97421..b537818c65 100644
--- a/network/wsNetwork.go
+++ b/network/wsNetwork.go
@@ -101,6 +101,9 @@ const slowWritingPeerMonitorInterval = 5 * time.Second
// to the log file. Note that the log file itself would also json-encode these before placing them in the log file.
const unprintableCharacterGlyph = "▯"
+// match config.PublicAddress to this string to automatically set PublicAddress from Address()
+const autoconfigPublicAddress = "auto"
+
var networkIncomingConnections = metrics.MakeGauge(metrics.NetworkIncomingConnections)
var networkOutgoingConnections = metrics.MakeGauge(metrics.NetworkOutgoingConnections)
@@ -113,6 +116,9 @@ var networkBroadcastSendMicros = metrics.MakeCounter(metrics.MetricName{Name: "a
var networkBroadcastsDropped = metrics.MakeCounter(metrics.MetricName{Name: "algod_broadcasts_dropped_total", Description: "number of broadcast messages not sent to any peer"})
var networkPeerBroadcastDropped = metrics.MakeCounter(metrics.MetricName{Name: "algod_peer_broadcast_dropped_total", Description: "number of broadcast messages not sent to some peer"})
+var networkPeerIdentityDisconnect = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_identity_duplicate", Description: "number of times identity challenge cause us to disconnect a peer"})
+var networkPeerIdentityError = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_identity_error", Description: "number of times an error occurs (besides expected) when processing identity challenges"})
+
var networkSlowPeerDrops = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_slow_drops_total", Description: "number of peers dropped for being slow to send to"})
var networkIdlePeerDrops = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_idle_drops_total", Description: "number of peers dropped due to idle connection"})
var networkBroadcastQueueFull = metrics.MakeCounter(metrics.MetricName{Name: "algod_network_broadcast_queue_full_total", Description: "number of messages that were drops due to full broadcast queue"})
@@ -141,6 +147,8 @@ const peerShutdownDisconnectionAckDuration = 50 * time.Millisecond
type Peer interface{}
// PeerOption allows users to specify a subset of peers to query
+//
+//msgp:ignore PeerOption
type PeerOption int
const (
@@ -258,6 +266,8 @@ type OutgoingMessage struct {
}
// ForwardingPolicy is an enum indicating to whom we should send a message
+//
+//msgp:ignore ForwardingPolicy
type ForwardingPolicy int
const (
@@ -376,6 +386,10 @@ type WebsocketNetwork struct {
prioTracker *prioTracker
prioResponseChan chan *wsPeer
+ // identity challenge scheme for creating challenges and responding
+ identityScheme identityChallengeScheme
+ identityTracker identityTracker
+
// outgoingMessagesBufferSize is the size used for outgoing messages.
outgoingMessagesBufferSize int
@@ -741,6 +755,8 @@ func (wn *WebsocketNetwork) setup() {
config.Consensus[protocol.ConsensusCurrentVersion].DownCommitteeSize),
)
+ wn.identityTracker = NewIdentityTracker()
+
wn.broadcastQueueHighPrio = make(chan broadcastRequest, wn.outgoingMessagesBufferSize)
wn.broadcastQueueBulk = make(chan broadcastRequest, 100)
wn.meshUpdateRequests = make(chan meshRequest, 5)
@@ -823,6 +839,25 @@ func (wn *WebsocketNetwork) Start() {
} else {
wn.scheme = "http"
}
+
+ // if PublicAddress set to automatic, pull the name from Address()
+ if wn.config.PublicAddress == autoconfigPublicAddress {
+ addr, ok := wn.Address()
+ if ok {
+ url, err := url.Parse(addr)
+ if err == nil {
+ wn.config.PublicAddress = fmt.Sprintf("%s:%s", url.Hostname(), url.Port())
+ }
+ }
+ }
+ // if the network has a public address, use that as the name for connection deduplication
+ if wn.config.PublicAddress != "" {
+ wn.RegisterHandlers(identityHandlers)
+ }
+ if wn.identityScheme == nil && wn.config.PublicAddress != "" {
+ wn.identityScheme = NewIdentityChallengeScheme(wn.config.PublicAddress)
+ }
+
wn.meshUpdateRequests <- meshRequest{false, nil}
if wn.prioScheme != nil {
wn.RegisterHandlers(prioHandlers)
@@ -1144,6 +1179,20 @@ func (wn *WebsocketNetwork) ServeHTTP(response http.ResponseWriter, request *htt
challenge = wn.prioScheme.NewPrioChallenge()
responseHeader.Set(PriorityChallengeHeader, challenge)
}
+
+ localAddr, _ := wn.Address()
+ var peerIDChallenge identityChallengeValue
+ var peerID crypto.PublicKey
+ if wn.identityScheme != nil {
+ var err error
+ peerIDChallenge, peerID, err = wn.identityScheme.VerifyRequestAndAttachResponse(responseHeader, request.Header)
+ if err != nil {
+ networkPeerIdentityError.Inc(nil)
+ wn.log.With("err", err).With("remote", trackedRequest.otherPublicAddr).With("local", localAddr).Warnf("peer (%s) supplied an invalid identity challenge, abandoning peering", trackedRequest.otherPublicAddr)
+ return
+ }
+ }
+
conn, err := wn.upgrader.Upgrade(response, request, responseHeader)
if err != nil {
wn.log.Info("ws upgrade fail ", err)
@@ -1165,12 +1214,14 @@ func (wn *WebsocketNetwork) ServeHTTP(response http.ResponseWriter, request *htt
prioChallenge: challenge,
createTime: trackedRequest.created,
version: matchingVersion,
+ identity: peerID,
+ identityChallenge: peerIDChallenge,
+ identityVerified: 0,
features: decodePeerFeatures(matchingVersion, request.Header.Get(PeerFeaturesHeader)),
}
peer.TelemetryGUID = trackedRequest.otherTelemetryGUID
peer.init(wn.config, wn.outgoingMessagesBufferSize)
wn.addPeer(peer)
- localAddr, _ := wn.Address()
wn.log.With("event", "ConnectedIn").With("remote", trackedRequest.otherPublicAddr).With("local", localAddr).Infof("Accepted incoming connection from peer %s", trackedRequest.otherPublicAddr)
wn.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent,
telemetryspec.PeerEventDetails{
@@ -1701,7 +1752,7 @@ func (wn *WebsocketNetwork) checkNewConnectionsNeeded() bool {
newAddrs := wn.phonebook.GetAddresses(desired+numOutgoingTotal, PhoneBookEntryRelayRole)
for _, na := range newAddrs {
if na == wn.config.PublicAddress {
- // filter out self-public address, so we won't try to connect to outselves.
+ // filter out self-public address, so we won't try to connect to ourselves.
continue
}
gossipAddr, ok := wn.tryConnectReserveAddr(na)
@@ -1957,6 +2008,9 @@ const InstanceNameHeader = "X-Algorand-InstanceName"
// PriorityChallengeHeader HTTP header informs a client about the challenge it should sign to increase network priority.
const PriorityChallengeHeader = "X-Algorand-PriorityChallenge"
+// IdentityChallengeHeader is used to exchange IdentityChallenges
+const IdentityChallengeHeader = "X-Algorand-IdentityChallenge"
+
// TooManyRequestsRetryAfterHeader HTTP header let the client know when to make the next connection attempt
const TooManyRequestsRetryAfterHeader = "Retry-After"
@@ -2113,6 +2167,12 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) {
for _, supportedProtocolVersion := range wn.supportedProtocolVersions {
requestHeader.Add(ProtocolAcceptVersionHeader, supportedProtocolVersion)
}
+
+ var idChallenge identityChallengeValue
+ if wn.identityScheme != nil {
+ idChallenge = wn.identityScheme.AttachChallenge(requestHeader, addr)
+ }
+
// for backward compatibility, include the ProtocolVersion header as well.
requestHeader.Set(ProtocolVersionHeader, wn.protocolVersion)
// set the features header (comma-separated list)
@@ -2164,13 +2224,41 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) {
return
}
+ // if we abort before making a wsPeer this cleanup logic will close the connection
+ closeEarly := func(msg string) {
+ deadline := time.Now().Add(peerDisconnectionAckDuration)
+ err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseProtocolError, msg), deadline)
+ if err != nil {
+ wn.log.Infof("tryConnect: failed to write CloseMessage to connection for %s", conn.RemoteAddr().String())
+ }
+ err = conn.CloseWithoutFlush()
+ if err != nil {
+ wn.log.Infof("tryConnect: failed to CloseWithoutFlush to connection for %s", conn.RemoteAddr().String())
+ }
+ }
+
// no need to test the response.StatusCode since we know it's going to be http.StatusSwitchingProtocols, as it's already being tested inside websocketDialer.DialContext.
// we need to examine the headers here to extract which protocol version we should be using.
responseHeaderOk, matchingVersion := wn.checkServerResponseVariables(response.Header, gossipAddr)
if !responseHeaderOk {
// The error was already logged, so no need to log again.
+ closeEarly("Unsupported headers")
return
}
+ localAddr, _ := wn.Address()
+
+ var peerID crypto.PublicKey
+ var idVerificationMessage []byte
+ if wn.identityScheme != nil {
+ // if the peer responded with an identity challenge response, but it can't be verified, don't proceed with peering
+ peerID, idVerificationMessage, err = wn.identityScheme.VerifyResponse(response.Header, idChallenge)
+ if err != nil {
+ networkPeerIdentityError.Inc(nil)
+ wn.log.With("err", err).With("remote", addr).With("local", localAddr).Warn("peer supplied an invalid identity response, abandoning peering")
+ closeEarly("Invalid identity response")
+ return
+ }
+ }
throttledConnection := false
if atomic.AddInt32(&wn.throttledOutgoingConnections, int32(-1)) >= 0 {
@@ -2188,12 +2276,28 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) {
connMonitor: wn.connPerfMonitor,
throttledOutgoingConnection: throttledConnection,
version: matchingVersion,
+ identity: peerID,
features: decodePeerFeatures(matchingVersion, response.Header.Get(PeerFeaturesHeader)),
}
peer.TelemetryGUID, peer.InstanceName, _ = getCommonHeaders(response.Header)
+
+ // if there is a final verification message to send, it means this peer has a verified identity,
+ // attempt to set the peer and identityTracker
+ if len(idVerificationMessage) > 0 {
+ atomic.StoreUint32(&peer.identityVerified, uint32(1))
+ wn.peersLock.Lock()
+ ok := wn.identityTracker.setIdentity(peer)
+ wn.peersLock.Unlock()
+ if !ok {
+ networkPeerIdentityDisconnect.Inc(nil)
+ wn.log.With("remote", addr).With("local", localAddr).Warn("peer deduplicated before adding because the identity is already known")
+ closeEarly("Duplicate connection")
+ return
+ }
+ }
peer.init(wn.config, wn.outgoingMessagesBufferSize)
wn.addPeer(peer)
- localAddr, _ := wn.Address()
+
wn.log.With("event", "ConnectedOut").With("remote", addr).With("local", localAddr).Infof("Made outgoing connection to peer %v", addr)
wn.log.EventWithDetails(telemetryspec.Network, telemetryspec.ConnectPeerEvent,
telemetryspec.PeerEventDetails{
@@ -2206,6 +2310,14 @@ func (wn *WebsocketNetwork) tryConnect(addr, gossipAddr string) {
wn.maybeSendMessagesOfInterest(peer, nil)
+ // if there is a final identification verification message to send, send it to the peer
+ if len(idVerificationMessage) > 0 {
+ sent := peer.writeNonBlock(context.Background(), idVerificationMessage, true, crypto.Digest{}, time.Now())
+ if !sent {
+ wn.log.With("remote", addr).With("local", localAddr).Warn("could not send identity challenge verification")
+ }
+ }
+
peers.Set(uint64(wn.NumPeers()))
outgoingPeers.Set(uint64(wn.numOutgoingPeers()))
@@ -2333,6 +2445,7 @@ func (wn *WebsocketNetwork) removePeer(peer *wsPeer, reason disconnectReason) {
if peer.peerIndex < len(wn.peers) && wn.peers[peer.peerIndex] == peer {
heap.Remove(peersHeap{wn}, peer.peerIndex)
wn.prioTracker.removePeer(peer)
+ wn.identityTracker.removeIdentity(peer)
if peer.throttledOutgoingConnection {
atomic.AddInt32(&wn.throttledOutgoingConnections, int32(1))
}
@@ -2344,6 +2457,8 @@ func (wn *WebsocketNetwork) removePeer(peer *wsPeer, reason disconnectReason) {
func (wn *WebsocketNetwork) addPeer(peer *wsPeer) {
wn.peersLock.Lock()
defer wn.peersLock.Unlock()
+ // simple duplicate *pointer* check. should never trigger given the callers to addPeer
+ // TODO: remove this after making sure it is safe to do so
for _, p := range wn.peers {
if p == peer {
wn.log.Errorf("dup peer added %#v", peer)
diff --git a/network/wsNetwork_test.go b/network/wsNetwork_test.go
index ac9a757214..c86eed11a5 100644
--- a/network/wsNetwork_test.go
+++ b/network/wsNetwork_test.go
@@ -19,6 +19,7 @@ package network
import (
"bytes"
"context"
+ "encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
@@ -119,7 +120,7 @@ func init() {
defaultConfig.MaxConnectionsPerIP = 30
}
-func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local) *WebsocketNetwork {
+func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local, opts ...testWebsocketOption) *WebsocketNetwork {
log := logging.TestingLog(t)
log.SetLevel(logging.Level(conf.BaseLoggerDebugLevel))
wn := &WebsocketNetwork{
@@ -129,13 +130,32 @@ func makeTestWebsocketNodeWithConfig(t testing.TB, conf config.Local) *Websocket
GenesisID: "go-test-network-genesis",
NetworkID: config.Devtestnet,
}
+ // apply options to newly-created WebsocketNetwork, if provided
+ for _, opt := range opts {
+ opt.applyOpt(wn)
+ }
+
wn.setup()
wn.eventualReadyDelay = time.Second
return wn
}
-func makeTestWebsocketNode(t testing.TB) *WebsocketNetwork {
- return makeTestWebsocketNodeWithConfig(t, defaultConfig)
+// interface for providing extra options to makeTestWebsocketNode
+type testWebsocketOption interface {
+ applyOpt(wn *WebsocketNetwork)
+}
+
+// option to add KV to wn base logger
+type testWebsocketLogNameOption struct{ logName string }
+
+func (o testWebsocketLogNameOption) applyOpt(wn *WebsocketNetwork) {
+ if o.logName != "" {
+ wn.log = wn.log.With("name", o.logName)
+ }
+}
+
+func makeTestWebsocketNode(t testing.TB, opts ...testWebsocketOption) *WebsocketNetwork {
+ return makeTestWebsocketNodeWithConfig(t, defaultConfig, opts...)
}
type messageCounterHandler struct {
@@ -1111,6 +1131,900 @@ func TestGetPeers(t *testing.T) {
assert.Equal(t, expectAddrs, peerAddrs)
}
+// confirms that if the config PublicAddress is set to "auto",
+// PublicAddress is loaded when possible with the value of Address()
+func TestAutoPublicAddress(t *testing.T) {
+ partitiontest.PartitionTest(t)
+ t.Parallel()
+
+ netA := makeTestWebsocketNode(t)
+ netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ netA.Start()
+
+ time.Sleep(100 * time.Millisecond)
+
+ // check that "auto" has been overloaded
+ addr, ok := netA.Address()
+ addr = hostAndPort(addr)
+ require.True(t, ok)
+ require.NotEqual(t, "auto", netA.PublicAddress())
+ require.Equal(t, addr, netA.PublicAddress())
+}
+
+// mock an identityTracker
+type mockIdentityTracker struct {
+ isOccupied bool
+ setCount int
+ insertCount int
+ removeCount int
+ lock deadlock.Mutex
+ realTracker identityTracker
+}
+
+func newMockIdentityTracker(realTracker identityTracker) *mockIdentityTracker {
+ return &mockIdentityTracker{
+ isOccupied: false,
+ setCount: 0,
+ insertCount: 0,
+ removeCount: 0,
+ realTracker: realTracker,
+ }
+}
+
+func (d *mockIdentityTracker) setIsOccupied(b bool) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.isOccupied = b
+}
+func (d *mockIdentityTracker) removeIdentity(p *wsPeer) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.removeCount++
+ d.realTracker.removeIdentity(p)
+}
+func (d *mockIdentityTracker) getInsertCount() int {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ return d.insertCount
+}
+func (d *mockIdentityTracker) getRemoveCount() int {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ return d.removeCount
+}
+func (d *mockIdentityTracker) getSetCount() int {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ return d.setCount
+}
+func (d *mockIdentityTracker) setIdentity(p *wsPeer) bool {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.setCount++
+ // isOccupied is true, meaning we're overloading the "ok" return to false
+ if d.isOccupied {
+ return false
+ }
+ ret := d.realTracker.setIdentity(p)
+ if ret {
+ d.insertCount++
+ }
+ return ret
+}
+
+func hostAndPort(u string) string {
+ url, err := url.Parse(u)
+ if err == nil {
+ return fmt.Sprintf("%s:%s", url.Hostname(), url.Port())
+ }
+ return ""
+}
+
+// TestPeeringWithIdentityChallenge tests the happy path of connecting with identity challenge:
+// - both peers have correctly set PublicAddress
+// - both should exchange identities and verify
+// - both peers should be able to deduplicate connections
+func TestPeeringWithIdentityChallenge(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"})
+ netA.identityTracker = newMockIdentityTracker(netA.identityTracker)
+ netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"})
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ netB.config.PublicAddress = "auto"
+ netB.config.GossipFanout = 1
+
+ netA.Start()
+ defer netA.Stop()
+ netB.Start()
+ defer netB.Stop()
+
+ addrA, ok := netA.Address()
+ require.True(t, ok)
+ gossipA, err := netA.addrToGossipAddr(addrA)
+ require.NoError(t, err)
+
+ addrB, ok := netB.Address()
+ require.True(t, ok)
+ gossipB, err := netB.addrToGossipAddr(addrB)
+ require.NoError(t, err)
+
+ // set addresses to just host:port to match phonebook/dns format
+ addrA = hostAndPort(addrA)
+ addrB = hostAndPort(addrB)
+
+ // first connection should work just fine
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ // just one A->B connection
+ assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut)))
+
+ // confirm identity map was added to for both hosts
+ assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getInsertCount())
+
+ // netB has to wait for a final verification message over WS Handler, so pause a moment
+ time.Sleep(250 * time.Millisecond)
+ assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount())
+
+ // bi-directional connection from B should not proceed
+ if _, ok := netB.tryConnectReserveAddr(addrA); ok {
+ netB.wg.Add(1)
+ netB.tryConnect(addrA, gossipA)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+
+ // still just one A->B connection
+ assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut)))
+ // netA never attempts to set identity as it never sees a verified identity
+ assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ // netB would attempt to add the identity to the tracker
+ // but it would not end up being added
+ assert.Equal(t, 2, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount())
+
+ // Check deduplication again, this time from A
+ // the "ok" from tryConnectReserveAddr is overloaded here because isConnectedTo
+ // will prevent this connection from attempting in the first place
+ // in the real world, that isConnectedTo doesn't always trigger, if the hosts are behind
+ // a load balancer or other NAT
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok || true {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+
+ // netB never tries to add a new identity, since the connection gets abandoned before it is verified
+ assert.Equal(t, 2, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount())
+ // still just one A->B connection
+ assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 2, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getInsertCount())
+
+ // Now have A connect to node C, which has the same PublicAddress as B (e.g., because it shares the
+ // same public load balancer endpoint). C will have a different identity keypair and so will not be
+ // considered a duplicate.
+ netC := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netC"})
+ netC.identityTracker = newMockIdentityTracker(netC.identityTracker)
+ netC.config.PublicAddress = addrB
+ netC.config.GossipFanout = 1
+
+ netC.Start()
+ defer netC.Stop()
+
+ addrC, ok := netC.Address()
+ require.True(t, ok)
+ gossipC, err := netC.addrToGossipAddr(addrC)
+ require.NoError(t, err)
+ addrC = hostAndPort(addrC)
+
+ // A connects to C (but uses addrB here to simulate case where B & C have the same PublicAddress)
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipC)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+
+ // A->B and A->C both open
+ assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 2, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netC.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut)))
+
+ // confirm identity map was added to for both hosts
+ assert.Equal(t, 3, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 2, netA.identityTracker.(*mockIdentityTracker).getInsertCount())
+
+ // netC has to wait for a final verification message over WS Handler, so pause a moment
+ time.Sleep(250 * time.Millisecond)
+ assert.Equal(t, 1, netC.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netC.identityTracker.(*mockIdentityTracker).getInsertCount())
+
+}
+
+// TestPeeringSenderIdentityChallengeOnly will confirm that if only the Sender
+// Uses Identity, no identity exchange happens in the connection
+func TestPeeringSenderIdentityChallengeOnly(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"})
+ netA.identityTracker = newMockIdentityTracker(netA.identityTracker)
+ netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"})
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ //netB.config.PublicAddress = "auto"
+ netB.config.GossipFanout = 1
+
+ netA.Start()
+ defer netA.Stop()
+ netB.Start()
+ defer netB.Stop()
+
+ addrA, ok := netA.Address()
+ require.True(t, ok)
+ gossipA, err := netA.addrToGossipAddr(addrA)
+ require.NoError(t, err)
+
+ addrB, ok := netB.Address()
+ require.True(t, ok)
+ gossipB, err := netB.addrToGossipAddr(addrB)
+ require.NoError(t, err)
+
+ // set addresses to just host:port to match phonebook/dns format
+ addrA = hostAndPort(addrA)
+ addrB = hostAndPort(addrB)
+
+ // first connection should work just fine
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+
+ // confirm identity map was not added to for either host
+ assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+
+ // bi-directional connection should also work
+ if _, ok := netB.tryConnectReserveAddr(addrA); ok {
+ netB.wg.Add(1)
+ netB.tryConnect(addrA, gossipA)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ // the nodes are connected redundantly
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedOut)))
+ // confirm identity map was not added to for either host
+ assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+}
+
+// TestPeeringReceiverIdentityChallengeOnly will confirm that if only the Receiver
+// Uses Identity, no identity exchange happens in the connection
+func TestPeeringReceiverIdentityChallengeOnly(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"})
+ netA.identityTracker = newMockIdentityTracker(netA.identityTracker)
+ //netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"})
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ netB.config.PublicAddress = "auto"
+ netB.config.GossipFanout = 1
+
+ netA.Start()
+ defer netA.Stop()
+ netB.Start()
+ defer netB.Stop()
+
+ addrA, ok := netA.Address()
+ require.True(t, ok)
+ gossipA, err := netA.addrToGossipAddr(addrA)
+ require.NoError(t, err)
+
+ addrB, ok := netB.Address()
+ require.True(t, ok)
+ gossipB, err := netB.addrToGossipAddr(addrB)
+ require.NoError(t, err)
+
+ // set addresses to just host:port to match phonebook/dns format
+ addrA = hostAndPort(addrA)
+ addrB = hostAndPort(addrB)
+
+ // first connection should work just fine
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ // single A->B connection
+ assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut)))
+
+ // confirm identity map was not added to for either host
+ assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+
+ // bi-directional connection should also work
+ if _, ok := netB.tryConnectReserveAddr(addrA); ok {
+ netB.wg.Add(1)
+ netB.tryConnect(addrA, gossipA)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedOut)))
+ // confirm identity map was not added to for either host
+ assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+}
+
+// TestPeeringIncorrectDeduplicationName confirm that if the reciever can't match
+// the Address in the challenge to its PublicAddress, identities aren't exchanged, but peering continues
+func TestPeeringIncorrectDeduplicationName(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"})
+ netA.identityTracker = newMockIdentityTracker(netA.identityTracker)
+ netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"})
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ netB.config.PublicAddress = "no:3333"
+ netB.config.GossipFanout = 1
+
+ netA.Start()
+ defer netA.Stop()
+ netB.Start()
+ defer netB.Stop()
+
+ addrA, ok := netA.Address()
+ require.True(t, ok)
+ gossipA, err := netA.addrToGossipAddr(addrA)
+ require.NoError(t, err)
+
+ addrB, ok := netB.Address()
+ require.True(t, ok)
+ gossipB, err := netB.addrToGossipAddr(addrB)
+ require.NoError(t, err)
+
+ // set addresses to just host:port to match phonebook/dns format
+ addrA = hostAndPort(addrA)
+ addrB = hostAndPort(addrB)
+
+ // first connection should work just fine
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ // single A->B connection
+ assert.Equal(t, 0, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 0, len(netB.GetPeers(PeersConnectedOut)))
+
+ // confirm identity map was not added to for either host
+ // nor was "set" called at all
+ assert.Equal(t, 0, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 0, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+
+ // bi-directional connection should also work
+ // this second connection should set identities, because the reciever address matches now
+ if _, ok := netB.tryConnectReserveAddr(addrA); ok {
+ netB.wg.Add(1)
+ netB.tryConnect(addrA, gossipA)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ // confirm that at this point the identityTracker was called once per network
+ // and inserted once per network
+ assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getSetCount())
+ assert.Equal(t, 1, netA.identityTracker.(*mockIdentityTracker).getInsertCount())
+ assert.Equal(t, 1, netB.identityTracker.(*mockIdentityTracker).getInsertCount())
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, 1, len(netB.GetPeers(PeersConnectedOut)))
+}
+
+// make a mockIdentityScheme which can accept overloaded behavior
+// use this over the next few tests to check that when one peer misbehaves, peering continues/halts as expected
+type mockIdentityScheme struct {
+ t *testing.T
+ realScheme *identityChallengePublicKeyScheme
+ attachChallenge func(attach http.Header, addr string) identityChallengeValue
+ verifyAndAttachResponse func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error)
+ verifyResponse func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error)
+}
+
+func newMockIdentityScheme(t *testing.T) *mockIdentityScheme {
+ return &mockIdentityScheme{t: t, realScheme: NewIdentityChallengeScheme("any")}
+}
+func (i mockIdentityScheme) AttachChallenge(attach http.Header, addr string) identityChallengeValue {
+ if i.attachChallenge != nil {
+ return i.attachChallenge(attach, addr)
+ }
+ return i.realScheme.AttachChallenge(attach, addr)
+}
+func (i mockIdentityScheme) VerifyRequestAndAttachResponse(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) {
+ if i.verifyAndAttachResponse != nil {
+ return i.verifyAndAttachResponse(attach, h)
+ }
+ return i.realScheme.VerifyRequestAndAttachResponse(attach, h)
+}
+func (i mockIdentityScheme) VerifyResponse(h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) {
+ if i.verifyResponse != nil {
+ return i.verifyResponse(i.t, h, c)
+ }
+ return i.realScheme.VerifyResponse(h, c)
+}
+
+// when the identity challenge is misconstructed in various ways, peering should behave as expected
+func TestPeeringWithBadIdentityChallenge(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ type testCase struct {
+ name string
+ attachChallenge func(attach http.Header, addr string) identityChallengeValue
+ totalInA int
+ totalOutA int
+ totalInB int
+ totalOutB int
+ }
+
+ testCases := []testCase{
+ // when identityChallenge is not included, peering continues as normal
+ {
+ name: "not included",
+ attachChallenge: func(attach http.Header, addr string) identityChallengeValue { return identityChallengeValue{} },
+ totalInA: 0,
+ totalOutA: 1,
+ totalInB: 1,
+ totalOutB: 0,
+ },
+ // when the identityChallenge is malformed B64, peering halts
+ {
+ name: "malformed b64",
+ attachChallenge: func(attach http.Header, addr string) identityChallengeValue {
+ attach.Add(IdentityChallengeHeader, "this does not decode!")
+ return newIdentityChallengeValue()
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ // when the identityChallenge can't be unmarshalled, peering halts
+ {
+ name: "not msgp decodable",
+ attachChallenge: func(attach http.Header, addr string) identityChallengeValue {
+ attach.Add(IdentityChallengeHeader, base64.StdEncoding.EncodeToString([]byte("Bad!Data!")))
+ return newIdentityChallengeValue()
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ // when the incorrect address is used, peering continues
+ {
+ name: "incorrect address",
+ attachChallenge: func(attach http.Header, addr string) identityChallengeValue {
+ s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys
+ c := identityChallenge{
+ Key: s.identityKeys.SignatureVerifier,
+ Challenge: newIdentityChallengeValue(),
+ PublicAddress: []byte("incorrect address!"),
+ }
+ attach.Add(IdentityChallengeHeader, c.signAndEncodeB64(s.identityKeys))
+ return c.Challenge
+ },
+ totalInA: 0,
+ totalOutA: 1,
+ totalInB: 1,
+ totalOutB: 0,
+ },
+ // when the challenge is incorrectly signed, peering halts
+ {
+ name: "bad signature",
+ attachChallenge: func(attach http.Header, addr string) identityChallengeValue {
+ s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys
+ c := identityChallenge{
+ Key: s.identityKeys.SignatureVerifier,
+ Challenge: newIdentityChallengeValue(),
+ PublicAddress: []byte("incorrect address!"),
+ }.Sign(s.identityKeys)
+ c.Msg.Challenge = newIdentityChallengeValue() // change the challenge after signing the message, so the signature check fails
+ enc := protocol.Encode(&c)
+ b64enc := base64.StdEncoding.EncodeToString(enc)
+ attach.Add(IdentityChallengeHeader, b64enc)
+ return c.Msg.Challenge
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Logf("Running Peering with Identity Challenge Test: %s", tc.name)
+ netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"})
+ netA.identityTracker = newMockIdentityTracker(netA.identityTracker)
+ netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ scheme := newMockIdentityScheme(t)
+ scheme.attachChallenge = tc.attachChallenge
+ netA.identityScheme = scheme
+
+ netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"})
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ netB.config.PublicAddress = "auto"
+ netB.config.GossipFanout = 1
+
+ netA.Start()
+ defer netA.Stop()
+ netB.Start()
+ defer netB.Stop()
+
+ addrB, ok := netB.Address()
+ require.True(t, ok)
+ gossipB, err := netB.addrToGossipAddr(addrB)
+ require.NoError(t, err)
+
+ // set addresses to just host:port to match phonebook/dns format
+ addrB = hostAndPort(addrB)
+
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ assert.Equal(t, tc.totalInA, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, tc.totalOutA, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, tc.totalInB, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, tc.totalOutB, len(netB.GetPeers(PeersConnectedOut)))
+ }
+
+}
+
+// when the identity challenge response is misconstructed in various way, confirm peering behaves as expected
+func TestPeeringWithBadIdentityChallengeResponse(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ type testCase struct {
+ name string
+ verifyAndAttachResponse func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error)
+ totalInA int
+ totalOutA int
+ totalInB int
+ totalOutB int
+ }
+
+ testCases := []testCase{
+ // when there is no response to the identity challenge, peering should continue without ID
+ {
+ name: "not included",
+ verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) {
+ return identityChallengeValue{}, crypto.PublicKey{}, nil
+ },
+ totalInA: 0,
+ totalOutA: 1,
+ totalInB: 1,
+ totalOutB: 0,
+ },
+ // when the response is malformed, do not peer
+ {
+ name: "malformed b64",
+ verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) {
+ attach.Add(IdentityChallengeHeader, "this does not decode!")
+ return identityChallengeValue{}, crypto.PublicKey{}, nil
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ // when the response is malformed, do not peer
+ {
+ name: "not msgp decodable",
+ verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) {
+ attach.Add(IdentityChallengeHeader, base64.StdEncoding.EncodeToString([]byte("Bad!Data!")))
+ return identityChallengeValue{}, crypto.PublicKey{}, nil
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ // when the original challenge isn't included, do not peer
+ {
+ name: "incorrect original challenge",
+ verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) {
+ s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys
+ // decode the header to an identityChallenge
+ msg, _ := base64.StdEncoding.DecodeString(h.Get(IdentityChallengeHeader))
+ idChal := identityChallenge{}
+ protocol.Decode(msg, &idChal)
+ // make the response object, with an incorrect challenge encode it and attach it to the header
+ r := identityChallengeResponse{
+ Key: s.identityKeys.SignatureVerifier,
+ Challenge: newIdentityChallengeValue(),
+ ResponseChallenge: newIdentityChallengeValue(),
+ }
+ attach.Add(IdentityChallengeHeader, r.signAndEncodeB64(s.identityKeys))
+ return r.ResponseChallenge, idChal.Key, nil
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ // when the message is incorrectly signed, do not peer
+ {
+ name: "bad signature",
+ verifyAndAttachResponse: func(attach http.Header, h http.Header) (identityChallengeValue, crypto.PublicKey, error) {
+ s := NewIdentityChallengeScheme("does not matter") // make a scheme to use its keys
+ // decode the header to an identityChallenge
+ msg, _ := base64.StdEncoding.DecodeString(h.Get(IdentityChallengeHeader))
+ idChal := identityChallenge{}
+ protocol.Decode(msg, &idChal)
+ // make the response object, then change the signature and encode and attach
+ r := identityChallengeResponse{
+ Key: s.identityKeys.SignatureVerifier,
+ Challenge: newIdentityChallengeValue(),
+ ResponseChallenge: newIdentityChallengeValue(),
+ }.Sign(s.identityKeys)
+ r.Msg.ResponseChallenge = newIdentityChallengeValue() // change the challenge after signing the message
+ enc := protocol.Encode(&r)
+ b64enc := base64.StdEncoding.EncodeToString(enc)
+ attach.Add(IdentityChallengeHeader, b64enc)
+ return r.Msg.ResponseChallenge, idChal.Key, nil
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Logf("Running Peering with Identity Challenge Response Test: %s", tc.name)
+ netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"})
+ netA.identityTracker = newMockIdentityTracker(netA.identityTracker)
+ netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"})
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ netB.config.PublicAddress = "auto"
+ netB.config.GossipFanout = 1
+
+ scheme := newMockIdentityScheme(t)
+ scheme.verifyAndAttachResponse = tc.verifyAndAttachResponse
+ netB.identityScheme = scheme
+
+ netA.Start()
+ defer netA.Stop()
+ netB.Start()
+ defer netB.Stop()
+
+ addrB, ok := netB.Address()
+ require.True(t, ok)
+ gossipB, err := netB.addrToGossipAddr(addrB)
+ require.NoError(t, err)
+
+ // set addresses to just host:port to match phonebook/dns format
+ addrB = hostAndPort(addrB)
+
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+ assert.Equal(t, tc.totalInA, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, tc.totalOutA, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, tc.totalInB, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, tc.totalOutB, len(netB.GetPeers(PeersConnectedOut)))
+ }
+
+}
+
+// when the identity challenge verification is misconstructed in various ways, peering should behave as expected
+func TestPeeringWithBadIdentityVerification(t *testing.T) {
+ partitiontest.PartitionTest(t)
+
+ type testCase struct {
+ name string
+ verifyResponse func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error)
+ totalInA int
+ totalOutA int
+ totalInB int
+ totalOutB int
+ additionalSleep time.Duration
+ occupied bool
+ }
+
+ testCases := []testCase{
+ // in a totally unmodified scenario, the two peers stay connected even after the verification timeout
+ {
+ name: "happy path",
+ totalInA: 0,
+ totalOutA: 1,
+ totalInB: 1,
+ totalOutB: 0,
+ },
+ // if the peer does not send a final message, the peers stay connected
+ {
+ name: "not included",
+ verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) {
+ return crypto.PublicKey{}, []byte{}, nil
+ },
+ totalInA: 0,
+ totalOutA: 1,
+ totalInB: 1,
+ totalOutB: 0,
+ },
+ // when the identityVerification can't be unmarshalled, peer is disconnected
+ {
+ name: "not msgp decodable",
+ verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) {
+ message := append([]byte(protocol.NetIDVerificationTag), []byte("Bad!Data!")[:]...)
+ return crypto.PublicKey{}, message, nil
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ {
+ // when the verification signature doesn't match the peer's expectation (the previously exchanged identity), peer is disconnected
+ name: "bad signature",
+ verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) {
+ headerString := h.Get(IdentityChallengeHeader)
+ require.NotEmpty(t, headerString)
+ msg, err := base64.StdEncoding.DecodeString(headerString)
+ require.NoError(t, err)
+ resp := identityChallengeResponseSigned{}
+ err = protocol.Decode(msg, &resp)
+ require.NoError(t, err)
+ s := NewIdentityChallengeScheme("does not matter") // make a throwaway key
+ ver := identityVerificationMessageSigned{
+ // fill in correct ResponseChallenge field
+ Msg: identityVerificationMessage{ResponseChallenge: resp.Msg.ResponseChallenge},
+ Signature: s.identityKeys.SignBytes([]byte("bad bytes for signing")),
+ }
+ message := append([]byte(protocol.NetIDVerificationTag), protocol.Encode(&ver)[:]...)
+ return crypto.PublicKey{}, message, nil
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ {
+ // when the verification signature doesn't match the peer's expectation (the previously exchanged identity), peer is disconnected
+ name: "bad signature",
+ verifyResponse: func(t *testing.T, h http.Header, c identityChallengeValue) (crypto.PublicKey, []byte, error) {
+ s := NewIdentityChallengeScheme("does not matter") // make a throwaway key
+ ver := identityVerificationMessageSigned{
+ // fill in wrong ResponseChallenge field
+ Msg: identityVerificationMessage{ResponseChallenge: newIdentityChallengeValue()},
+ Signature: s.identityKeys.SignBytes([]byte("bad bytes for signing")),
+ }
+ message := append([]byte(protocol.NetIDVerificationTag), protocol.Encode(&ver)[:]...)
+ return crypto.PublicKey{}, message, nil
+ },
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ },
+ {
+ // when the identity is already in use, peer is disconnected
+ name: "identity occupied",
+ verifyResponse: nil,
+ totalInA: 0,
+ totalOutA: 0,
+ totalInB: 0,
+ totalOutB: 0,
+ occupied: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Logf("Running Peering with Identity Verification Test: %s", tc.name)
+ netA := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netA"})
+ netA.identityTracker = newMockIdentityTracker(netA.identityTracker)
+ netA.config.PublicAddress = "auto"
+ netA.config.GossipFanout = 1
+
+ scheme := newMockIdentityScheme(t)
+ scheme.verifyResponse = tc.verifyResponse
+ netA.identityScheme = scheme
+
+ netB := makeTestWebsocketNode(t, testWebsocketLogNameOption{"netB"})
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ netB.config.PublicAddress = "auto"
+ netB.config.GossipFanout = 1
+ // if the key is occupied, make the tracker fail to insert the peer
+ if tc.occupied {
+ netB.identityTracker = newMockIdentityTracker(netB.identityTracker)
+ netB.identityTracker.(*mockIdentityTracker).setIsOccupied(true)
+ }
+
+ netA.Start()
+ defer netA.Stop()
+ netB.Start()
+ defer netB.Stop()
+
+ addrB, ok := netB.Address()
+ require.True(t, ok)
+ gossipB, err := netB.addrToGossipAddr(addrB)
+ require.NoError(t, err)
+
+ // set addresses to just host:port to match phonebook/dns format
+ addrB = hostAndPort(addrB)
+
+ if _, ok := netA.tryConnectReserveAddr(addrB); ok {
+ netA.wg.Add(1)
+ netA.tryConnect(addrB, gossipB)
+ // let the tryConnect go forward
+ time.Sleep(250 * time.Millisecond)
+ }
+
+ assert.Equal(t, tc.totalInA, len(netA.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, tc.totalOutA, len(netA.GetPeers(PeersConnectedOut)))
+ assert.Equal(t, tc.totalInB, len(netB.GetPeers(PeersConnectedIn)))
+ assert.Equal(t, tc.totalOutB, len(netB.GetPeers(PeersConnectedOut)))
+ }
+}
+
type benchmarkHandler struct {
returns chan uint64
}
diff --git a/network/wsPeer.go b/network/wsPeer.go
index d769d122b7..accd06cf9e 100644
--- a/network/wsPeer.go
+++ b/network/wsPeer.go
@@ -105,17 +105,18 @@ var unknownProtocolTagMessagesTotal = metrics.MakeCounter(metrics.UnknownProtoco
// defaultSendMessageTags is the default list of messages which a peer would
// allow to be sent without receiving any explicit request.
var defaultSendMessageTags = map[protocol.Tag]bool{
- protocol.AgreementVoteTag: true,
- protocol.MsgDigestSkipTag: true,
- protocol.NetPrioResponseTag: true,
- protocol.PingTag: true,
- protocol.PingReplyTag: true,
- protocol.ProposalPayloadTag: true,
- protocol.TopicMsgRespTag: true,
- protocol.MsgOfInterestTag: true,
- protocol.TxnTag: true,
- protocol.UniEnsBlockReqTag: true,
- protocol.VoteBundleTag: true,
+ protocol.AgreementVoteTag: true,
+ protocol.MsgDigestSkipTag: true,
+ protocol.NetPrioResponseTag: true,
+ protocol.NetIDVerificationTag: true,
+ protocol.PingTag: true,
+ protocol.PingReplyTag: true,
+ protocol.ProposalPayloadTag: true,
+ protocol.TopicMsgRespTag: true,
+ protocol.MsgOfInterestTag: true,
+ protocol.TxnTag: true,
+ protocol.UniEnsBlockReqTag: true,
+ protocol.VoteBundleTag: true,
}
// interface allows substituting debug implementation for *websocket.Conn
@@ -165,6 +166,8 @@ const disconnectLeastPerformingPeer disconnectReason = "LeastPerformingPeer"
const disconnectCliqueResolve disconnectReason = "CliqueResolving"
const disconnectRequestReceived disconnectReason = "DisconnectRequest"
const disconnectStaleWrite disconnectReason = "DisconnectStaleWrite"
+const disconnectDuplicateConnection disconnectReason = "DuplicateConnection"
+const disconnectBadIdentityData disconnectReason = "BadIdentityData"
// Response is the structure holding the response from the server
type Response struct {
@@ -233,6 +236,12 @@ type wsPeer struct {
// is present in wn.peers.
peerIndex int
+ // the peer's identity key which it uses for identityChallenge exchanges
+ identity crypto.PublicKey
+ identityVerified uint32
+ // the identityChallenge is recorded to the peer so it may verify its identity at a later time
+ identityChallenge identityChallengeValue
+
// Challenge sent to the peer on an incoming connection
prioChallenge string
@@ -336,8 +345,7 @@ func (wp *wsPeer) Version() string {
return wp.version
}
-// Unicast sends the given bytes to this specific peer. Does not wait for message to be sent.
-//
+// Unicast sends the given bytes to this specific peer. Does not wait for message to be sent.
// (Implements UnicastPeer)
func (wp *wsPeer) Unicast(ctx context.Context, msg []byte, tag protocol.Tag) error {
var err error
@@ -571,7 +579,7 @@ func (wp *wsPeer) readLoop() {
atomic.AddUint64(&wp.ppMessageCount, 1)
// the remaining valid tags: no special handling here
case protocol.NetPrioResponseTag, protocol.PingTag, protocol.PingReplyTag,
- protocol.StateProofSigTag, protocol.UniEnsBlockReqTag, protocol.VoteBundleTag:
+ protocol.StateProofSigTag, protocol.UniEnsBlockReqTag, protocol.VoteBundleTag, protocol.NetIDVerificationTag:
default: // unrecognized tag
unknownProtocolTagMessagesTotal.Inc(nil)
atomic.AddUint64(&wp.unkMessageCount, 1)
@@ -1013,6 +1021,7 @@ func (wp *wsPeer) OnClose(f func()) {
wp.closers = append(wp.closers, f)
}
+//msgp:ignore peerFeatureFlag
type peerFeatureFlag int
const pfCompressedProposal peerFeatureFlag = 1
diff --git a/protocol/hash.go b/protocol/hash.go
index 079333a438..26975a402f 100644
--- a/protocol/hash.go
+++ b/protocol/hash.go
@@ -48,6 +48,9 @@ const (
MerkleArrayNode HashID = "MA"
MerkleVectorCommitmentBottomLeaf HashID = "MB"
Message HashID = "MX"
+ NetIdentityChallenge HashID = "NIC"
+ NetIdentityChallengeResponse HashID = "NIR"
+ NetIdentityVerificationMessage HashID = "NIV"
NetPrioResponse HashID = "NPR"
OneTimeSigKey1 HashID = "OT1"
OneTimeSigKey2 HashID = "OT2"
diff --git a/protocol/tags.go b/protocol/tags.go
index 876a8c868c..ef44e74acf 100644
--- a/protocol/tags.go
+++ b/protocol/tags.go
@@ -25,16 +25,17 @@ type Tag string
// are encoded using a comma separator (see network/msgOfInterest.go).
// The tags must be 2 bytes long.
const (
- AgreementVoteTag Tag = "AV"
- MsgOfInterestTag Tag = "MI"
- MsgDigestSkipTag Tag = "MS"
- NetPrioResponseTag Tag = "NP"
- PingTag Tag = "pi"
- PingReplyTag Tag = "pj"
- ProposalPayloadTag Tag = "PP"
- StateProofSigTag Tag = "SP"
- TopicMsgRespTag Tag = "TS"
- TxnTag Tag = "TX"
+ AgreementVoteTag Tag = "AV"
+ MsgOfInterestTag Tag = "MI"
+ MsgDigestSkipTag Tag = "MS"
+ NetPrioResponseTag Tag = "NP"
+ NetIDVerificationTag Tag = "NI"
+ PingTag Tag = "pi"
+ PingReplyTag Tag = "pj"
+ ProposalPayloadTag Tag = "PP"
+ StateProofSigTag Tag = "SP"
+ TopicMsgRespTag Tag = "TS"
+ TxnTag Tag = "TX"
//UniCatchupReqTag Tag = "UC" was replaced by UniEnsBlockReqTag
UniEnsBlockReqTag Tag = "UE"
//UniEnsBlockResTag Tag = "US" was used for wsfetcherservice
@@ -47,6 +48,7 @@ var TagList = []Tag{
AgreementVoteTag,
MsgOfInterestTag,
MsgDigestSkipTag,
+ NetIDVerificationTag,
NetPrioResponseTag,
PingTag,
PingReplyTag,