Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions peer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ A quick overview of the major features peer provides are as follows:
- Provides a basic concurrent safe Decred peer for handling Decred
communications via the peer-to-peer protocol
- Full duplex reading and writing of Decred protocol messages
- Automatic handling of the initial handshake process including protocol
version negotiation
- Separate synchronous method for the initial handshake process which includes
protocol version negotiation
- Asynchronous message queueing of outbound messages with optional channel for
notification when the message is actually sent
- Flexible peer configuration
Expand All @@ -48,17 +48,17 @@ A quick overview of the major features peer provides are as follows:
- These could all be sent manually via the standard message output function,
but the helpers provide additional nice functionality such as duplicate
filtering and address randomization
- Ability to wait for shutdown/disconnect
- Context-aware Run method for all asynchronous I/O processing that blocks until disconnect
- Comprehensive test coverage

## Installation and Updating

This package is part of the `github.com/decred/dcrd/peer/v2` module. Use the
This package is part of the `github.com/decred/dcrd/peer/v3` module. Use the
standard go tooling for working with modules to incorporate it.

## Examples

* [New Outbound Peer Example](https://pkg.go.dev/github.com/decred/dcrd/peer/v2#example-package-NewOutboundPeer)
* [New Outbound Peer Example](https://pkg.go.dev/github.com/decred/dcrd/peer/v3#example-package-NewOutboundPeer)
Demonstrates the basic process for initializing and creating an outbound peer.
Peers negotiate by exchanging version and verack messages. For demonstration,
a simple handler for the version message is attached to the peer.
Expand Down
78 changes: 41 additions & 37 deletions peer/doc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2016-2022 The Decred developers
// Copyright (c) 2016-2026 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand Down Expand Up @@ -61,80 +61,84 @@ This provides high flexibility for things such as connecting via proxies, acting
as a proxy, creating bridge peers, choosing whether to listen for inbound peers,
etc.

NewOutboundPeer and NewInboundPeer functions must be followed by calling Connect
with a net.Conn instance to the peer. This will start all async I/O goroutines
and initiate the protocol negotiation process. Once finished with the peer call
Disconnect to disconnect from the peer and clean up all resources.
WaitForDisconnect can be used to block until peer disconnection and resource
cleanup has completed.
[NewOutboundPeer] and [NewInboundPeer] must be followed by calling
[Peer.Handshake] on the returned instance to perform the initial protocol
negotiation handshake process and finally [Peer.Run] to start all async I/O
goroutines and block until peer disconnection and resource cleanup has
completed.

When finished with the peer, call [Peer.Disconnect] or cancel the context
provided to [Peer.Run] to close the connection and clean up all resources.

# Callbacks

In order to do anything useful with a peer, it is necessary to react to decred
messages. This is accomplished by creating an instance of the MessageListeners
struct with the callbacks to be invoke specified and setting the Listeners field
of the Config struct specified when creating a peer to it.
messages. This is accomplished by creating an instance of the [MessageListeners]
struct with the callbacks to be invoke specified and setting [Config.Listeners]
in the [Config] struct specified when creating a peer.

For convenience, a callback hook for all of the currently supported decred
messages is exposed which receives the peer instance and the concrete message
type. In addition, a hook for OnRead is provided so even custom messages types
for which this package does not directly provide a hook, as long as they
implement the wire.Message interface, can be used. Finally, the OnWrite hook
is provided, which in conjunction with OnRead, can be used to track server-wide
byte counts.
type. In addition, a [MessageListeners.OnRead] hook is provided so even custom
messages types for which this package does not directly provide a hook, as long
as they implement the wire.Message interface, can be used. Finally, the
[MessageListeners.OnWrite] hook is provided, which in conjunction with
[MessageListeners.OnRead], can be used to track server-wide byte counts.

It is often useful to use closures which encapsulate state when specifying the
callback handlers. This provides a clean method for accessing that state when
callbacks are invoked.

# Queuing Messages and Inventory

The QueueMessage function provides the fundamental means to send messages to the
remote peer. As the name implies, this employs a non-blocking queue. A done
channel which will be notified when the message is actually sent can optionally
be specified. There are certain message types which are better sent using other
functions which provide additional functionality.
The [Peer.QueueMessage] function provides the fundamental means to send messages
to the remote peer. As the name implies, this employs a non-blocking queue. A
done channel which will be notified when the message is actually sent can
optionally be specified. There are certain message types which are better sent
using other functions which provide additional functionality.

Of special interest are inventory messages. Rather than manually sending MsgInv
messages via Queuemessage, the inventory vectors should be queued using the
QueueInventory function. It employs batching and trickling along with
intelligent known remote peer inventory detection and avoidance through the use
of a most-recently used algorithm.
messages via [Peer.QueueMessage], the inventory vectors should be queued using
the [Peer.QueueInventory] function. It employs batching and trickling along
with intelligent known remote peer inventory detection and avoidance through the
use of a most-recently used algorithm.

# Message Sending Helper Functions

In addition to the bare QueueMessage function previously described, the
PushAddrMsg, PushGetBlocksMsg, and PushGetHeadersMsg functions are provided as a
convenience. While it is of course possible to create and send these messages
manually via QueueMessage, these helper functions provided additional useful
functionality that is typically desired.
In addition to the bare [Peer.QueueMessage] function previously described, the
[Peer.PushAddrMsg], [Peer.PushGetBlocksMsg], and [Peer.PushGetHeadersMsg]
functions are provided as a convenience. While it is of course possible to
create and send these messages manually via [Peer.QueueMessage], these helper
functions provided additional useful functionality that is typically desired.

For example, the PushAddrMsg function automatically limits the addresses to the
For example, [Peer.PushAddrMsg] automatically limits the addresses to the
maximum number allowed by the message and randomizes the chosen addresses when
there are too many. This allows the caller to simply provide a slice of known
addresses, such as that returned by the addrmgr package, without having to worry
about the details.

Finally, the PushGetBlocksMsg and PushGetHeadersMsg functions will construct
Finally, [Peer.PushGetBlocksMsg] and [Peer.PushGetHeadersMsg] will construct
proper messages using a block locator and ignore back to back duplicate
requests.

# Peer Statistics

A snapshot of the current peer statistics can be obtained with the StatsSnapshot
function. This includes statistics such as the total number of bytes read and
written, the remote address, user agent, and negotiated protocol version.
A snapshot of the current peer statistics can be obtained with
[Peer.StatsSnapshot]. This includes statistics such as the total number of
bytes read and written, the remote address, user agent, and negotiated protocol
version.

# Logging

This package provides extensive logging capabilities through the UseLogger
This package provides extensive logging capabilities through the [UseLogger]
function which allows a slog.Logger to be specified. For example, logging at
the debug level provides summaries of every message sent and received, and
logging at the trace level provides full dumps of parsed messages as well as the
raw message bytes using a format similar to hexdump -C.

# Improvement Proposals
# Decred Change Proposals

This package supports all improvement proposals supported by the wire package.
This package supports all Decred Change Proposals (DCPs) supported by the wire
package.
*/
package peer
60 changes: 60 additions & 0 deletions peer/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2026 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package peer

// ErrorKind identifies a kind of error. It has full support for errors.Is
// and errors.As, so the caller can directly check against an error kind
// when determining the reason for an error.
type ErrorKind string

// These constants are used to identify a specific [Error].
const (
// ErrNotVersionMessage indicates the first message received from a remote
// peer is not the required version message.
ErrNotVersionMessage = ErrorKind("ErrNotVersionMessage")

// ErrSelfConnection indicates a peer attempted to connect to itself.
ErrSelfConnection = ErrorKind("ErrSelfConnection")

// ErrProtocolVerTooOld indicates a protocol version is older than the
// minimum required version.
ErrProtocolVerTooOld = ErrorKind("ErrProtocolVerTooOld")

// ErrNotVerAckMessage indicates the second message received from a remote
// peer is not the required verack message.
ErrNotVerAckMessage = ErrorKind("ErrNotVerAckMessage")

// ErrHandshakeTimeout indicates the initial handshake timed out before
// completing.
ErrHandshakeTimeout = ErrorKind("ErrHandshakeTimeout")
)

// Error satisfies the error interface and prints human-readable errors.
func (e ErrorKind) Error() string {
return string(e)
}

// Error identifies an address manager error. It has full support for
// errors.Is and errors.As, so the caller can ascertain the specific reason
// for the error by checking the underlying error.
type Error struct {
Err error
Description string
}

// Error satisfies the error interface and prints human-readable errors.
func (e Error) Error() string {
return e.Description
}

// Unwrap returns the underlying wrapped error.
func (e Error) Unwrap() error {
return e.Err
}

// makeError creates an [Error] given a set of arguments.
func makeError(kind ErrorKind, desc string) Error {
return Error{Err: kind, Description: desc}
}
129 changes: 129 additions & 0 deletions peer/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) 2026 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package peer

import (
"errors"
"testing"
)

// TestErrorKindStringer tests the stringized output for the [ErrorKind] type.
func TestErrorKindStringer(t *testing.T) {
t.Parallel()

tests := []struct {
in ErrorKind
want string
}{
{ErrNotVersionMessage, "ErrNotVersionMessage"},
{ErrSelfConnection, "ErrSelfConnection"},
{ErrProtocolVerTooOld, "ErrProtocolVerTooOld"},
{ErrNotVerAckMessage, "ErrNotVerAckMessage"},
{ErrHandshakeTimeout, "ErrHandshakeTimeout"},
}

for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("#%d: got: %s want: %s", i, result, test.want)
continue
}
}
}

// TestError tests the error output for the [Error] type.
func TestError(t *testing.T) {
t.Parallel()

tests := []struct {
in Error
want string
}{{
Error{Description: "some error"},
"some error",
}, {
Error{Description: "human-readable error"},
"human-readable error",
}}

for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("#%d: got: %s want: %s", i, result, test.want)
continue
}
}
}

// TestErrorKindIsAs ensures both [ErrorKind] and [Error] can be identified as
// being a specific error kind via [errors.Is] and unwrapped via [errors.As].
func TestErrorKindIsAs(t *testing.T) {
tests := []struct {
name string
err error
target error
wantMatch bool
wantAs ErrorKind
}{{
name: "ErrNotVersionMessage == ErrNotVersionMessage",
err: ErrNotVersionMessage,
target: ErrNotVersionMessage,
wantMatch: true,
wantAs: ErrNotVersionMessage,
}, {
name: "Error.ErrNotVersionMessage == ErrNotVersionMessage",
err: makeError(ErrNotVersionMessage, ""),
target: ErrNotVersionMessage,
wantMatch: true,
wantAs: ErrNotVersionMessage,
}, {
name: "ErrNotVersionMessage != ErrSelfConnection",
err: ErrNotVersionMessage,
target: ErrSelfConnection,
wantMatch: false,
wantAs: ErrNotVersionMessage,
}, {
name: "Error.ErrNotVersionMessage != ErrSelfConnection",
err: makeError(ErrNotVersionMessage, ""),
target: ErrSelfConnection,
wantMatch: false,
wantAs: ErrNotVersionMessage,
}, {
name: "ErrNotVersionMessage != Error.ErrSelfConnection",
err: ErrNotVersionMessage,
target: makeError(ErrSelfConnection, ""),
wantMatch: false,
wantAs: ErrNotVersionMessage,
}, {
name: "Error.ErrNotVersionMessage != Error.ErrSelfConnection",
err: makeError(ErrNotVersionMessage, ""),
target: makeError(ErrSelfConnection, ""),
wantMatch: false,
wantAs: ErrNotVersionMessage,
}}

for _, test := range tests {
// Ensure the error matches or not depending on the expected result.
result := errors.Is(test.err, test.target)
if result != test.wantMatch {
t.Errorf("%s: incorrect error identification -- got %v, want %v",
test.name, result, test.wantMatch)
continue
}

// Ensure the underlying error kind can be unwrapped and is the
// expected kind.
var kind ErrorKind
if !errors.As(test.err, &kind) {
t.Errorf("%s: unable to unwrap to error kind", test.name)
continue
}
if kind != test.wantAs {
t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v",
test.name, kind, test.wantAs)
continue
}
}
}
Loading