go-p2p/docs/development.md
Snider 3dd204705a docs: graduate TODO/FINDINGS into production documentation
Replace internal task tracking (TODO.md, FINDINGS.md) with structured
documentation in docs/. Trim CLAUDE.md to agent instructions only.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 15:01:55 +00:00

7.4 KiB

Development Guide — go-p2p

Prerequisites

  • Go 1.25 or later (the module declares go 1.25.5)
  • Network access to forge.lthn.ai for private dependencies (Borg, Poindexter, Enchantrix)
  • SSH key configured for git@forge.lthn.ai:2223 (HTTPS auth is not supported on Forge)

Private modules are hosted at forge.lthn.ai. Ensure your GONOSUMCHECK or GONOSUMDB environment variable includes forge.lthn.ai if sum database verification fails for those paths, and that GOPRIVATE=forge.lthn.ai is set so the Go toolchain does not proxy them through proxy.golang.org.

Build and Test

# Run all tests
go test ./...

# Run a single test by name
go test -run TestName ./...

# Run tests with race detector (required before any PR)
go test -race ./...

# Skip integration tests (they bind real TCP ports)
go test -short ./...

# Run benchmarks
go test -bench . ./...
go test -bench BenchmarkName ./...

# Coverage per package
go test -cover ./node
go test -cover ./ueps
go test -cover ./logging

# Coverage report (HTML)
go test -coverprofile=cover.out ./... && go tool cover -html=cover.out

# Static analysis
go vet ./...

Test Patterns

Table-Driven Subtests

All tests use table-driven subtests with t.Run(). A test that does not follow this pattern should be refactored before merging.

func TestFoo(t *testing.T) {
    cases := []struct {
        name    string
        input   string
        want    string
        wantErr bool
    }{
        {name: "valid input", input: "abc", want: "ABC"},
        {name: "empty input", input: "", wantErr: true},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            got, err := Foo(tc.input)
            if tc.wantErr {
                require.Error(t, err)
                return
            }
            require.NoError(t, err)
            assert.Equal(t, tc.want, got)
        })
    }
}

Test Naming Suffixes

Inherited from the wider go-p2p test tradition:

Suffix Meaning
_Good Happy path
_Bad Expected error conditions
_Ugly Panic or edge-case conditions

Assertions

Use github.com/stretchr/testify. Import both assert (non-fatal) and require (fatal on failure):

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

Use require for setup steps and preconditions. Use assert for verification steps where partial results are still informative.

Transport Test Helper

The node package provides a reusable helper for tests that need two live transport endpoints:

tp := setupTestTransportPair(t)
// tp.Server, tp.Client     — *Transport
// tp.ServerNode, tp.ClientNode — *NodeManager
// tp.ServerReg, tp.ClientReg  — *PeerRegistry

pc := tp.connectClient(t) // performs handshake, returns *PeerConnection

setupTestTransportPairWithConfig accepts custom TransportConfig for each side, useful for testing keepalive and rate limiting behaviours.

The helper registers a t.Cleanup function that calls Stop() on both transports, so tests do not need to manage teardown.

Integration Tests

Integration tests are gated with testing.Short():

if testing.Short() {
    t.Skip("skipping integration test in short mode")
}

Run them explicitly with go test ./... (without -short). They bind real localhost TCP ports and are safe to run in parallel with distinct transports because each test uses an ephemeral listen address (:0-style via net/http/httptest internally).

Benchmark Structure

Benchmarks live in bench_test.go files within each package. They follow the standard Go benchmark pattern:

func BenchmarkFoo(b *testing.B) {
    // setup outside loop
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Foo()
    }
}

Run with -benchmem to track allocations:

go test -bench . -benchmem ./...

Reference timings (Apple M-series, 2025):

Benchmark Time Allocs
Identity keygen 217 µs
Shared secret derivation 53 µs
Message serialise 4 µs
SMSG encrypt+decrypt 4.7 µs
Challenge sign+verify 505 ns
KD-tree peer select 349 ns
KD-tree rebuild 2.5 µs
UEPS marshal 621 ns
UEPS read+verify 1 µs
bufpool get/put 8 ns 0
Challenge generation 211 ns

Coding Standards

UK English

All identifiers, comments, log messages, and documentation must use UK English spellings:

  • colour (not color)
  • organisation (not organization)
  • centre (not center)
  • behaviour (not behavior)
  • recognise (not recognize)

Strict Types

All parameters and return types must carry explicit type annotations. Avoid interface{} except where a generic pool or JSON-raw interface genuinely requires it; prefer any (the Go 1.18 alias) if you must. Do not use blank identifiers to discard typed return values without good reason.

Error Handling

  • Never discard errors silently.
  • Wrap errors with context using fmt.Errorf("context: %w", err).
  • Return typed sentinel errors for conditions callers need to inspect programmatically.

Licence Header

Every new file must carry the EUPL-1.2 licence identifier. The module's LICENSE file governs the package. Do not include the full licence text in each file; a short SPDX identifier comment at the top is sufficient for new files:

// SPDX-License-Identifier: EUPL-1.2

Security-First

  • HMAC verification is required on all wire traffic (UEPS frames, not negotiable).
  • Challenge-response authentication must not be weakened or bypassed in tests; use the setupTestTransportPair helper, which performs a real handshake.
  • Any code that extracts archives must use extractTarball (or equivalent defensive logic) with Zip Slip defence, symlink rejection, and a size limit.
  • Rate limiting and deduplication are not optional features; they are core to the security posture.

Logging

Use the logging package throughout. Do not use fmt.Println or log.Printf in library code.

logging.Debug("connected to peer", logging.Fields{"peer_id": pc.Peer.ID})
logging.Warn("peer rate limited", logging.Fields{"peer_id": pc.Peer.ID})

For hot paths (read loop), use the debug log sampling pattern already established in transport.go to avoid flooding logs:

if debugLogCounter.Add(1)%debugLogInterval == 0 {
    logging.Debug("received message", logging.Fields{...})
}

Conventional Commits

All commits follow the Conventional Commits specification:

type(scope): short description

Body (optional): longer explanation of the why, not the what.

Co-Authored-By: Virgil <virgil@lethean.io>

Types: feat, fix, test, refactor, docs, chore, perf, ci

Scopes: node, ueps, logging, transport, peer, dispatcher, identity, bundle, controller

Examples:

feat(dispatcher): implement UEPS threat circuit breaker

test(transport): add keepalive timeout and MaxConns enforcement tests

fix(peer): prevent data race in GracefulClose (P2P-RACE-1)

Forge Remote

The canonical remote is:

ssh://git@forge.lthn.ai:2223/core/go-p2p.git

Push to forge remote only. GitHub remotes are disabled for push.

Dependency Management

After adding or removing a dependency:

go mod tidy
go work sync   # if working within the go-p2p workspace

Do not vendor dependencies. The module uses the standard module proxy for public packages and Forge for private ones.