Skip to content

Conversation

Roasbeef
Copy link
Member

I was working on creating some ad hoc tests to examine the p2p performance behavior of some new PRs I was working on, and reached for the peer.Brontide package. However, it fell short, as the package is very tightly coupled to all the sub-systems it interacts with, and also carries several assumptions re the way that lnd is set up.

This PR is an attempt to introduce a new pacakge lnp2p, that implements the bare essentials of p2p connectivity (brontide handshake, init sending, ping handling, etc). This can be used to write ad hoc tests, and even expand our integration tests to test things like protocol violations, as we can use this package to basically MiTM between sub systems.

I've also built on top of my actor package to show what a version of the peer as an actor would look like. The final version is pretty barebores, but it's easy to see how goroutines like the ping checker can actually be implemented as an actor that uses a MessageSink to only filter for ping messages, then issues a disconnect message if violated, etc, etc.

Keeping it in draft for now, as it'll likely evolve as I continue to write these p2p tests.

Diff looks large, but it's mostly tests as I was shooting for 90% + test coverage. Likely some duplication there as well.

Ideally we can eventually use this directly in peer.Brontide, but I think we shouldn't rush quite into that, as we've seen the recent impact of p2p bugs and how that can destabalize nodes. So for now, we can have it just be a package for experiments and tests.

This commit establishes the foundational interfaces and types for the
new P2P connection layer. The design prioritizes flexibility and
testability by defining clean abstractions that decouple the P2P
layer from lnd's internal subsystems.

The key interfaces introduced include BrontideConn for abstracting
encrypted connections, P2PConnection for high-level connection
management, and supporting types for connection state management,
node addressing, key generation, and connection lifecycle events.

These interfaces enable multiple implementation strategies, from
simple direct connections for testing to sophisticated actor-based
systems for production use.
SimplePeer provides a lightweight implementation of the P2PConnection
interface that focuses on simplicity and ease of use. This implementation
handles the essential P2P operations including connection establishment,
message exchange, and graceful disconnection.

The design uses Go iterators for message streaming, providing a clean
API for consuming incoming messages. It includes built-in ping/pong
handling, connection state management, and event notification for
monitoring connection lifecycle changes.

SimplePeer also supports an optional broadcast mode that integrates
with lnd's msgmux router, enabling dual-mode message handling where
messages can be consumed both through the iterator pattern and routed
to registered handlers simultaneously.
This commit introduces PeerActor, a sophisticated actor-based implementation
of the P2PConnection interface that leverages lnd's actor framework for
concurrent message processing and distribution.

The implementation features the MessageSink pattern, which allows service
keys to register with optional message filtering predicates. This enables
fine-grained control over which messages each handler receives, supporting
scenarios where different subsystems need to process only specific message
types or messages matching certain criteria.

PeerActor manages connection lifecycle through actor messages, distributes
incoming messages to registered service keys in parallel, and provides
actor-based APIs for service key management. The design prioritizes
scalability and concurrent processing while maintaining clean separation
between connection management and message handling logic.
This commit establishes a robust testing foundation for the P2P layer
by introducing mock implementations and test harnesses that simplify
test setup and reduce boilerplate.

The test infrastructure includes mockP2PConnection for simulating
P2P connections with configurable behaviors, mockBrontideConn for
testing at the transport layer, and a sophisticated test harness
system that provides helper methods for common test patterns. The
harness manages actor systems, connection expectations, and service
key registration, enabling concise and expressive tests.

A key addition is the newTestPubKey() helper that eliminates verbose
key generation patterns throughout tests, improving readability. The
infrastructure supports both simple unit tests and complex integration
scenarios with multiple peers and concurrent operations.
This commit introduces comprehensive unit tests that validate the
core functionality of both SimplePeer and PeerActor implementations.
The tests leverage the previously added test infrastructure to
minimize boilerplate while ensuring thorough coverage.

For SimplePeer, the tests verify connection lifecycle, message
exchange, state transitions, address handling, ping/pong responses,
broadcast mode integration, and various error conditions. Special
attention is given to timeout handling and proper cleanup on
disconnection.

For PeerActor, the tests cover message distribution to service keys,
the MessageSink filtering mechanism, concurrent operations, service
key management, and proper actor lifecycle handling. The tests also
validate the refreshActorRefs mechanism that handles dynamic service
registration and deregistration.
The integration tests validate complex multi-component scenarios
that exercise the full P2P stack. These tests go beyond unit testing
to verify that the various components work correctly together in
realistic usage patterns.

SimplePeer integration tests include establishing real connections
with mock servers, handling connection failures and retries, message
streaming with iterators, and init message exchange validation. The
tests also cover the broadcast mode where messages are simultaneously
consumed through iterators and routed to msgmux handlers.

PeerActor integration tests focus on distributed message processing,
including ping/pong internal handling, error and warning message
distribution to multiple handlers, dynamic service key registration
during message processing, and proper cleanup when actors disappear.
These tests ensure that the actor-based message distribution maintains
correctness under concurrent operations.
This commit provides practical examples demonstrating how to use the
P2P layer in various scenarios. The examples serve as both documentation
and functional tests, showing real-world usage patterns that developers
can adapt for their needs.

The examples cover SimplePeer usage for basic connections and message
exchange, PeerActor setup with service key registration and message
filtering, broadcast mode configuration for dual message consumption,
custom connection behaviors with mock implementations, and proper
error handling and cleanup patterns. Each example is self-contained
and runnable as a test, ensuring they remain valid as the API evolves.

These examples are particularly valuable for developers integrating
the P2P layer into testing frameworks or building custom peer
behaviors for protocol experimentation.
This commit establishes lnp2p as a standalone Go module with its own
dependency management. The module is versioned independently from the
main lnd repository, allowing it to be imported and used by external
projects including testing frameworks and experimental implementations.

The dependencies are carefully curated to include only what's necessary
for the P2P layer functionality, including lnd's actor framework for
the actor-based implementation, core lnd packages for wire protocol
and cryptographic operations, and testing dependencies like testify
for the comprehensive test suite.

This modular approach enables the P2P layer to evolve independently
while maintaining compatibility with the broader Lightning Network
ecosystem.
The lnd/actor package provides the actor framework that powers the
concurrent message processing capabilities of the new P2P layer. This
dependency enables the PeerActor implementation to leverage actor-based
concurrency patterns for scalable message distribution.

The actor framework offers a robust foundation for building concurrent
systems with message-passing semantics, which aligns well with the
P2P layer's requirements for handling multiple message streams and
service registrations concurrently.
The test harness files were incorrectly named without the _test.go
suffix, causing build failures when compiling the package. These files
reference types that are only available in test builds (mockP2PConnection,
mockBrontideConn, etc.), so they must be marked as test files themselves.

This change renames test_harness.go to test_harness_test.go and
test_mock_harness.go to test_mock_harness_test.go, ensuring they're
only compiled during test builds when their dependencies are available.
The test suite continues to pass with full coverage.
@Roasbeef Roasbeef added p2p Code related to the peer-to-peer behaviour refactoring actors labels Sep 17, 2025
@Roasbeef
Copy link
Member Author

An example of the base API:

target, err := lnp2p.ParseNodeAddress("[email protected]:9735")
if err != nil {
    return err
}

cfg := lnp2p.SimplePeerConfig{
    KeyGenerator: &lnp2p.EphemeralKeyGenerator{},
    Target:       *target,
    Features:     lnp2p.DefaultFeatures(),
    Timeouts:     lnp2p.DefaultTimeouts(),
}

peer, err := lnp2p.NewSimplePeer(cfg)
if err != nil {
    return err
}
defer peer.Close()

if err := peer.Connect(context.Background()); err != nil {
    return err
}

This'll connect out, do the init dance, etc, etc. There's also a mode that lets you control exactly when messages are read off the wire, which can be useful for types of hybrid integration tests where we want to run assertions betwen messages, or simulate invalid protocol behavior.


There's an iterator that can be used to recv messages, and also connection events:

    for msg := range peer.ReceiveMessages() {
        switch m := msg.(type) {
        case *lnwire.ChannelUpdate:
            processUpdate(m)
        case *lnwire.Error:
            log.Printf("Peer error: %s", m.Data)
        case *lnwire.Ping:
        }
for event := range peer.ConnectionEvents() {
    log.Printf("[%s] State: %s", event.Timestamp, event.State)

    if event.State == lnp2p.StateConnected {
        // Connection established.
    } else if event.State == lnp2p.StateDisconnected && event.Error != nil {
        // Handle disconnection.
    }
}

There's also an API that's actor based. You can use it to allow any sub-system to register that messages shoud be sent to it, without obtaining a direct pointer/reference. A MessageSink can also use a filter pedicate to filter out which messages get sent to it.

target, err := lnp2p.ParseNodeAddress("[email protected]:9735")
if err != nil {
    return err
}

cfg := lnp2p.ActorWithConnConfig{
    SimplePeerCfg: lnp2p.SimplePeerConfig{
        KeyGenerator: &lnp2p.EphemeralKeyGenerator{},
        Target:       *target,
        Features:     lnp2p.DefaultFeatures(),
        Timeouts:     lnp2p.DefaultTimeouts(),
    },
    ActorSystem: system,
    ServiceKey:  channelKey,
    ActorName:   "channel-peer",
    MessageSinks: []*lnp2p.MessageSink{
        {
            ServiceKey: channelKey,
            Filter:     channelFilter,
        },
    },
}

peerActor, actorRef, err := lnp2p.NewActorWithConn(cfg)
if err != nil {
    return err
}

startResult := peerActor.Start(ctx)
if resp, err := startResult.Unpack(); err != nil || !resp.Success {
    return err
}

Here's a glimpse re how the actor system can be used to re-work the way sub-systems discover, and iteract with each other:

system := actor.NewActorSystem()
defer system.Shutdown()

// Make a new service key to handle channel update messges. 
channelKey := actor.NewServiceKey[lnp2p.PeerMessage, lnp2p.PeerResponse](
    "channel-handler",
)

// We'll use a simple handler that just processes the updates directly, and register it with the main system. 
behavior := actor.NewFunctionBehavior(func(ctx context.Context,  msg lnp2p.PeerMessage) fn.Result[lnp2p.PeerResponse] {
    if m, ok := msg.(*lnp2p.MessageReceived); ok {
        if update, ok := m.Message.(*lnwire.ChannelUpdate); ok {
            // Process channel update.
            processChannelUpdate(update)
            return fn.Ok[lnp2p.PeerResponse](
                &lnp2p.MessageReceivedAck{Processed: true},
            )
        }
    }
    return fn.Ok[lnp2p.PeerResponse](nil)
})
actor.RegisterWithSystem(system, "channel-handler", channelKey, behavior)


// Make a MessageSink with a filter, then register it w/ the peer actor. 
channelFilter := func(msg lnwire.Message) bool {
    switch msg.(type) {
    case *lnwire.ChannelUpdate, *lnwire.ChannelAnnouncement:
        return true
    default:
        return false
    }
}
updateSink := &lnp2p.MessageSink{
    ServiceKey: channelKey,
    Filter: channelFilter, 
}
success := peerActor.AddMessageSink(gossipSink)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

actors p2p Code related to the peer-to-peer behaviour refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant